Scala CLI
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10.0 (released 2026-10-12, the current Scala Next line) with Scala CLI 1.17.1 on JDK 26.
Every command whose output is shown below was run on that setup on 2026-10-15 and 2026-10-16; commands that are not run are labelled
"not run here" with the reason. Scala CLI itself is not tied to a Scala line: the directives and commands described here work the same with Scala 3.9 LTS and
3.3 LTS, except for the language features that are named with their release (the -preview example below is a 3.10 feature and does not compile on 3.9). The
Scala 3.10 and 3.9 compilers that Scala CLI launches need JDK 17 or later.
The scala command that comes with Scala 3 is Scala CLI, so everything on this page applies to
the command you installed and to the scala-cli binary alike. It is the
tool the rest of this section uses for examples: it needs no build file, resolves dependencies from a comment at the top of the source, and runs on
the first try. When a project outgrows it, it can export itself to an sbt or Mill build
(see sbt & Build Tools). The Java counterpart of "run one file without a build" is
java Hello.java; the Java build tools are covered on Java Build & Tooling.
The commands at a glance
Scala CLI is one executable with sub-commands. The ones in the first table are always available; the second table lists the ones that are
restricted, which means you must opt in with --power (once on the command line, or for good with scala config power true).
| Command | What it does |
|---|---|
|
Compile and run Scala code. It is the default when you pass inputs and no sub-command, so |
|
Compile only; prints warnings and errors, and nothing when everything is fine. |
|
Compile and run the tests with the configured test framework. |
|
Open the Scala REPL with the project and its dependencies on the classpath. |
|
Generate Scaladoc. |
|
Format code with Scalafmt. |
|
Delete the |
|
Write the BSP connection file that Metals and IntelliJ use. |
|
Like |
|
Housekeeping for the tool itself. |
Command (needs --power) |
What it does |
|---|---|
|
Build a launcher JAR, a library JAR, an assembly, a Docker image, a Scala.js file, a GraalVM native image or a Scala Native binary. |
|
Generate an sbt, Mill or JSON description of the project (experimental). |
|
Publish to Maven Central style repositories, or to the local repository. |
|
Run Scalafix rules, bump the dependency directives, create a project from a Giter8 template. |
|
Manage repository secrets for publishing from CI. |
The table was produced from scala --help and scala --power --help on this setup. Asking for a restricted command without
the flag fails with a message that tells you what to do, which is the first thing you will see when you try package:
The `package` sub-command is restricted.
You can run it with the `--power` flag or turn power mode on globally by running:
scala config power true
Running programs
The scenario for this page is a tiny trail planner. The smallest program is one file with a @main method, as on
Hello World & @main Methods; the interesting part is the first few lines, the
using directives. Here the directives pick the Scala version and the JVM, add compiler options and one library:
//> using scala 3.10.0
//> using jvm 17
//> using option -deprecation -Wunused:imports
//> using dep com.lihaoyi::os-lib:0.11.8
@main def trailhead(names: String*): Unit =
val greeting = names.mkString(", ")
println(s"Hello, $greeting! ${os.pwd.last}")
Run it, passing the program’s own arguments after --:
scala run . -- Ana Luis
Hello, Ana, Luis! proj
The . is the project directory: every .scala, .sc and .java file below it is part of the build, and the directives found in any of
them apply to the whole scope. You can also pass one file, several files or a directory. The first run downloads os-lib and its dependencies from
Maven Central through Coursier, which Scala CLI embeds; later runs use the local cache and an incremental compilation cache in .scala-build, so
later runs skip the work that has not changed.
//> using jvm 17 asks for a JVM by version. Scala CLI uses a matching JDK that is already installed and, if there is none,
downloads one through Coursier’s JVM index. On this machine a JDK 17 was installed, so nothing was downloaded; if yours has none,
the first run of the same file would fetch one (not run here, because it would download a JDK).
When more than one @main method exists in the inputs, Scala CLI refuses to guess:
error: Found several main classes: a, b
You can run one of them by passing it with the --main-class option, e.g.
scala run ../cls --server=false --main-class a
You can pick the main class interactively by passing the --interactive option.
scala run ../cls --server=false --interactive
Choose with --main-class (alias -M), or put //> using mainClass b in a source file. Running the same files with -M b prints b.
--list-main-classes prints the candidates. For one-liners there is -e:
scala run -e 'println(1 + 2)'
3
Two switches you will see in this section’s commands: --server=false skips the Bloop compilation server and compiles in the
scala process (handy in CI and sandboxes; the default build server makes repeated builds faster), and -v or -vv raise the verbosity.
Both are optional, and the examples elsewhere omit them for readability.
Source: Scala CLI: run
Using directives
A using directive is a comment of the form //> using key value placed before any code. Together the directives of a file replace a build
file for small projects: the same line works in a script, a test and the main source.
| Directive | Purpose |
|---|---|
|
The Scala version. Accepts a full version, a prefix such as |
|
The JVM to run on, by version or by id ( |
|
A dependency. |
|
A dependency that only the test scope sees. Many directives have a |
|
The Scala Toolkit (os-lib, uPickle, sttp, MUnit) in one directive; |
|
Compiler options (the plural |
|
The target platform: |
|
The class to run when several |
|
Add another source file to a script. |
|
A directory of resources on the classpath. |
|
JVM options for |
|
An extra Maven repository (here the Sonatype snapshots one; a URL also works). |
A dependency is written org::artifact:version: the double colon after the organization appends the Scala binary version, and a double colon before the version
(org::artifact::version) additionally appends the platform suffix for Scala.js or Scala Native libraries. The directive value is plain text, so it
needs no quoting. Directives that Scala CLI considers experimental or restricted are refused until you opt in. For example,
//> using objectWrapper in the file above, which forces the object wrapper for scripts, produced:
error: ./Trailhead.scala:5:11
error: Directives errors: The `//> using objectWrapper` directive is experimental.
error: You can run it with the `--power` flag or turn power mode on globally by running:
error: scala config power true
error: //> using objectWrapper
error: ^^^^^^^^^^^^^
Directives behave as follows:
-
They are read from every source file of the same scope (main or test) and merged, so a library declared in one file is visible in all of them. Only the target directives (
//> using target.scala 3,//> using target.platform js, …) apply to a single file. -
They must come before any
package,importor definition. -
Options given on the command line win over the directives in the sources.
-
To keep several files tidy, put the shared directives in a
project.scalafile that contains nothing else. The experimentalfixcommand can move them there for you. -
To pass the folder of the file to an option,
${.}is replaced by the directory of the file that contains the directive.
The older directive spellings, // using followed by a key in a plain comment, the @using annotation and //> using lib for libraries, belong to the 0.0.x series
and are not accepted any more; always write //> using dep.
The Toolkit directive is the one-liner for the "files, JSON, HTTP and tests" of the later pages of this section:
//> using toolkit 0.9.2
//> using test.toolkit 0.9.2
Toolkit 0.9.2 depends on os-lib 0.11.8, uPickle 4.4.3 and sttp client 4.0.21, and its test artifact adds MUnit 1.2.1 (these versions come from the
published pom). In sbt the same thing is libraryDependencies += "org.scala-lang" %% "toolkit" % "0.9.2" and the toolkit-test artifact
in Test; see sbt & Build Tools.
Scripts and shebangs
Files that end in .sc are scripts: any statement is allowed at the top level, the command-line arguments are in args, and the path of the
script is in scriptPath. A script with a directive and a dependency:
//> using scala 3.10.0
//> using dep com.lihaoyi::os-lib:0.11.8
val words = args.toList
println(s"args = $words")
println(s"total length = ${words.map(_.length).sum}")
println(os.pwd.last)
scala run stats.sc -- pine fir
args = List(pine, fir)
total length = 7
scripts
Source: Scala CLI: Scripts, Arguments
A script can pull in another source file with //> using file, and two scripts passed together can see each other (the names are the file names, so
hello.sc becomes the object hello). One restriction to know: you cannot reference the contents of a script called main.sc from another one,
because main clashes with the main method that every script wrapper contains.
A script that is its own executable starts with a shebang line, runs through the shebang sub-command and needs the executable bit:
#!/usr/bin/env -S scala shebang
//> using scala 3.10.0
println(s"Hello from a shebang script, args = ${args.toList}")
chmod +x hi.sc
./hi.sc one two
Hello from a shebang script, args = List(one, two)
Options for the run can follow shebang on the same line, for example #!/usr/bin/env -S scala shebang --scala-version 2.13. This example needs
scala on the PATH, which was arranged for the run above. A shebang script is the ideal replacement for a small shell script: it is typed,
and its dependencies download themselves.
Behind the scenes Scala CLI wraps the statements of a script in a class (Scala 3 default) or an object, and gives it a main method. The object wrapper is
used for Scala.js and can be forced with the experimental --object-wrapper flag; it can deadlock if a script waits on threads that start before its
initialization ends. The Scala 2 wrapper extends App.
Source: Scala CLI: Scripts
Compiling, warnings and compiler options
scala compile . prints compiler diagnostics and exits quietly on success. The options of //> using option go to the Scala 3 compiler
unchanged, so every flag of the next section of this documentation applies. A real warning with -Wunused:imports from the file above, after adding an unused import:
import scala.util.Random
def f = 1
-- [E198] Unused Symbol Warning: Unused.scala:1:18
1 | import scala.util.Random
| ^^^^^^
| unused import
1 warning found
The full list of warning and language switches is on Compiler Options, Linting & Errors.
Two switches deserve a mention here because they opt a build into unfinished language features. -preview enables features that are finished but not yet
on by default, and -experimental (or the scala.language.experimental imports, as in the 3.5.0 release announcement’s //> using options -source:future -language:experimental.modularity) allows experimental ones.
In Scala CLI they are ordinary compiler options. Partial function literals with parentheses (SIP-74) are a preview feature on 3.10. Without the option, the snippet
//> using scala 3.10.0
@main def pv(): Unit =
val xs = List((1, 1), (2, 5))
println(xs.collect(case (a, b) if b > 2 => a))
fails:
| ^^^^
| expression expected but case found
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
and with //> using option -preview as the second line it prints List(2). Preview and experimental features are described on
Language Evolution: SIPs, Preview & Experimental.
With //> using scala 3.9.0 and the same option the file fails with the same message, because the feature is not in 3.9.
Choosing the Scala version
The directive //> using scala or the options -S and --scala-version pick the compiler: 3.3.8 (the 3.3 LTS), 3.9.0
(the 3.9 LTS), 3.10.0, 2.13.18 or 2.12.21. A prefix such as 3.3 selects the newest patch release. Each version downloads its compiler on first use
(from Maven Central, via Coursier). scala.util.Properties.versionNumberString is a quick probe, with one catch: Scala 3 up to 3.7 reuses the Scala 2.13 library and reports
its version:
println(s"Scala library ${scala.util.Properties.versionNumberString}")
Scala library 2.13.18 <- with -S 3.3.8
Scala library 3.9.0 <- with -S 3.9.0
Scala library 3.10.0 <- with -S 3.10.0
Scala library 2.13.18 <- with -S 2.13.18
(The arrows are annotations added to the output, not part of it.) Scala 3.8 and later ship their own standard library, which is why 3.9.0 and 3.10.0 report themselves.
Testing
A source whose name ends in .test.scala, or that lives in a directory called test, or that declares //> using target.scope test, belongs to the
test scope. Add MUnit through the Toolkit and write a test as on Testing with MUnit:
//> using test.toolkit 0.9.2
class TrailSuite extends munit.FunSuite:
test("sum"):
assertEquals(List(1, 2, 3).sum, 6)
scala test .
TrailSuite:
+ sum 0.007s
scala test accepts --test-only 'TrailSuite' to choose suites and --test-framework (or the testFramework directive) when the framework cannot be detected.
MUnit, ScalaTest and JUnit are among the frameworks it can run. It fails with a non-zero exit code when a test fails, so
it is ready for CI.
Source: Scala CLI: test
Packaging (power mode)
package turns the project into something you can ship. The default output is a lightweight "bootstrap" JAR that downloads its dependencies at first start. For
a self-contained file use --assembly, for a JAR to put on a classpath use --library, and -o names the output. The -o option is itself restricted:
scala package . -o trailhead.jar --assembly
Unrecognized argument: The `-o` option is restricted.
You can run it with the `--power` flag or turn power mode on globally by running:
scala config power true
With the flag, which goes before the sub-command:
scala --power package . --assembly -o trailhead.jar -f
java -jar trailhead.jar Ana
Wrote ./trailhead.jar, run it with
./trailhead.jar
Hello, Ana! proj
The assembly was about 10 MB because it bundles os-lib, geny and the Scala library; the program also printed a JDK 26 sun.misc.Unsafe warning on stderr, which comes from the JVM and not from the program’s own code. The --library flag
produced a small JAR containing only the project’s classes. Other formats are available: --native-image (GraalVM, downloaded through Coursier’s JVM index),
--docker with --docker-image-repository, --js for a JavaScript file you run with Node, --native for a Scala Native executable, and --deb, --rpm, --pkg
or --msi for system installers (experimental). The Docker, GraalVM, Native and installer outputs were not run here: they need Docker, a GraalVM download,
a native toolchain or the platform packagers.
Source: Scala CLI: package
Documentation, formatting and the REPL
scala doc . -o apidoc -f writes Scaladoc for the project (-o chooses the output directory and -f overwrites it), and the last line of the run reports it:
Wrote Scaladoc to ./apidoc
The output is a static site with an index.html that you can open in a browser or publish; see Scaladoc.
scala fmt . formats with Scalafmt. If the project has no .scalafmt.conf, Scala CLI writes a temporary one under .scala-build with the Scalafmt version of the Scala CLI release
(3.11.5 here) and a dialect inferred from the Scala version. Create your own file with --save-scalafmt-conf or by hand. Running scala fmt --check . is the CI form: it exits with an error if any file would change. Scala CLI does not bundle Scalafmt;
on first use it downloads the Scalafmt native launcher for your platform from the Scalafmt GitHub release page, so the very first fmt needs access to github.com. On this machine that is
what happened, and the result for a messy file was:
object Messy {
def add(a:Int,b:Int)=a+b }
Source: Scala CLI: fmt
object Messy {
def add(a: Int, b: Int) = a + b
}
(Second block: the file after scala fmt Messy.scala.) Options for Scalafmt itself go after -F, for example scala fmt -F --version.
scala repl opens the REPL with the project’s dependencies on the classpath, and also accepts --dep to add one without editing a file. Fed from a pipe it
evaluates and prints, which is a quick way to try an expression:
printf 'List(1,2,3).map(_ * 2)\n:quit\n' | scala repl --dep com.lihaoyi::os-lib:0.11.8
Welcome to Scala 3.10.0 (26, Java Java HotSpot(TM) 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> val res0: List[Int] = List(2, 4, 6)
scala>
(Real output has colors when it goes to a terminal.) The REPL page, with worksheets and Scastie, is REPL, Worksheets & Scastie.
Source: Scala CLI: doc, Scala CLI: fmt, Scala CLI: repl
Setting up an IDE
scala setup-ide . writes the BSP connection file that Metals and IntelliJ read, and it also happens implicitly before run, compile and test. On Scala CLI 1.17.1
it wrote .bsp/scala.json (earlier documentation calls it scala-cli.json) and a set of ide-options-v2.json and related files under .scala-build. The details, and how to open the
directory in VS Code or IntelliJ, are on IDEs. Prefer directories to single files: an IDE shows every file below the opened folder but builds only
the ones Scala CLI knows about.
Source: Scala CLI: setup-ide
Platforms: Scala.js and Scala Native
A directive is enough to change the target. This script ran as JavaScript on Node 24, with the Scala.js linker downloaded from Maven Central:
//> using scala 3.10.0
//> using platform js
println("Hello from Scala.js: " + (1 to 3).map(_ * 2).mkString(","))
Hello from Scala.js: 2,4,6
Source: Scala CLI: Scala.js and Scala CLI: Directives, Platform
//> using platform native does the same for Scala Native, which also needs a C toolchain (clang) and was not run here. Library coordinates written with :: resolve to the
Scala.js or Native artifact automatically. More on the targets is on Scala.js & Scala Native.
Exporting to sbt or Mill
The export command (power mode, experimental) turns the directives into a conventional build, so you can start with Scala CLI and move later. For the project above:
scala --power export . --sbt -o exp-sbt
The `export` sub-command is experimental
Please bear in mind that non-ideal user experience should be expected.
Exporting to a sbt project...
Exported to: ./exp-sbt
The generated build.sbt carried the Scala version, the options and the dependencies, with the test dependencies in Test (the machine-specific
resolvers line that pointed at the local Scala distribution has been removed from the listing):
scalaVersion := "3.10.0"
scalacOptions ++= Seq("-deprecation", "-Wunused:imports")
testFrameworks += new TestFramework("munit.Framework")
libraryDependencies += "com.lihaoyi" %% "os-lib" % "0.11.8"
libraryDependencies ++= Seq(
"org.scala-lang" %% "toolkit" % "0.9.2" % Test,
"org.scala-lang" %% "toolkit-test" % "0.9.2" % Test,
"com.lihaoyi" %% "os-lib" % "0.11.8" % Test
)
--mill produces a build.mill. Be aware that it also writes the mill launcher scripts, which it downloads from the Mill GitHub repository; this was the case here and is
the reason that the Mill build file is described rather than listed. The exporter also copies the sources into a standard layout (src/main/scala and src/test/scala). Check the generated build against your needs before relying on it, since it is marked experimental.
Source: Scala CLI: export
Publishing (power mode)
publish builds the POM and artifacts and uploads them, publish local installs into the local Ivy or Maven repository, and publish setup writes the //> using publish. directives
directives (organization, name, version, license, developers, repository) into the project. Upload needs credentials and a repository, so none of these commands
was run here. For anything beyond a small library, sbt with a publishing plugin remains the more common route; see
sbt & Build Tools.
Where the books are outdated
-
Older material calls the tool
scala-cliand tells you to install it separately. With Scala 3.5 and later thescalacommand is Scala CLI; thescala-clibinary still exists. -
The directive syntaxes
// using,@usingand//> using libwere removed; today it is//> using depwith::for Scala libraries. -
Scripts were once run with the standalone Scala 2 script runner (
scala script.sc,scala -cp); with Scala CLIscala run script.sctakes directives and a shebang line ofscala shebang. -
Tutorials that start with
sbt newfor a first program are no longer the shortest path: one file andscala runis. -
Anything that says "Scala CLI cannot do X, use sbt" should be checked against the command list above:
test,package,publishandexportcover the common cases.