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

run

Compile and run Scala code. It is the default when you pass inputs and no sub-command, so scala Hello.scala means scala run Hello.scala.

compile

Compile only; prints warnings and errors, and nothing when everything is fine.

test

Compile and run the tests with the configured test framework.

repl (alias console)

Open the Scala REPL with the project and its dependencies on the classpath.

doc

Generate Scaladoc.

fmt (aliases format, scalafmt)

Format code with Scalafmt.

clean

Delete the .scala-build workspace directory.

setup-ide

Write the BSP connection file that Metals and IntelliJ use.

shebang

Like run, for shebang scripts.

config, version, update, uninstall

Housekeeping for the tool itself.

Command (needs --power) What it does

package

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.

export

Generate an sbt, Mill or JSON description of the project (experimental).

publish, publish local, publish setup

Publish to Maven Central style repositories, or to the local repository.

fix, dependency-update, new

Run Scalafix rules, bump the dependency directives, create a project from a Giter8 template.

github secret create, github secret list

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

//> using scala 3.10.0

The Scala version. Accepts a full version, a prefix such as 3.3 (the newest 3.3.x) or 3.nightly; Scala 2 versions work too.

//> using jvm 17

The JVM to run on, by version or by id (temurin:21).

//> using dep com.lihaoyi::os-lib:0.11.8

A dependency. :: appends the Scala binary version (os-lib_3), the equivalent of %% in sbt; one colon is a plain Java dependency (org.slf4j:slf4j-api:2.0.17).

//> using test.dep org.scalameta::munit::1.2.1

A dependency that only the test scope sees. Many directives have a test. twin.

//> using toolkit 0.9.2

The Scala Toolkit (os-lib, uPickle, sttp, MUnit) in one directive; //> using test.toolkit 0.9.2 limits it to tests.

//> using option -deprecation -Wunused:imports

Compiler options (the plural options is an alias).

//> using platform js

The target platform: jvm (default), js or native.

//> using mainClass hike

The class to run when several @main methods exist.

//> using file Util.scala

Add another source file to a script.

//> using resourceDir res

A directory of resources on the classpath.

//> using javaOpt -Xmx512m

JVM options for run and test.

//> using repository sonatype:snapshots

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, import or 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.scala file that contains nothing else. The experimental fix command 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

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.

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.

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.

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.

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

//> 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.

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-cli and tells you to install it separately. With Scala 3.5 and later the scala command is Scala CLI; the scala-cli binary still exists.

  • The directive syntaxes // using, @using and //> using lib were removed; today it is //> using dep with :: for Scala libraries.

  • Scripts were once run with the standalone Scala 2 script runner (scala script.sc, scala -cp); with Scala CLI scala run script.sc takes directives and a shebang line of scala shebang.

  • Tutorials that start with sbt new for a first program are no longer the shortest path: one file and scala run is.

  • Anything that says "Scala CLI cannot do X, use sbt" should be checked against the command list above: test, package, publish and export cover the common cases.