A command-line interface for executing Starlark scripts with rich module support, interactive mode, and web server capabilities.
StarCLI is a versatile tool that provides a convenient environment for running Starlark scripts from the command line. Starlark is a dialect of Python designed for configuration, extensibility, and embedding. StarCLI extends Starlark with additional modules and utilities to make it more powerful for various automation and scripting tasks.
-
Multiple Execution Modes:
- REPL (Read-Eval-Print Loop) for interactive script development
- Direct code execution from command line arguments
- Script file execution
- Web server mode that creates a Starbox environment for HTTP requests
-
Configurable Environment:
- Load custom modules
- Control recursion and global variable reassignment
- Set include paths for module loading
- Configure logging levels
-
Rich Module Support:
- Standard library modules (json, math, time, etc.)
- File system operations
- Network and HTTP requests
- Regular expressions
- Base64 encoding/decoding
- Email functionality
- LLM (Language Model) integration
- Templating, data formats & utilities (Liquid, TOML/YAML, QR codes, TOTP, emoji, in-memory cache)
- And many more
Download the archive for your system and checksums.txt from the same
GitHub Release. No Go installation is
needed to run a release binary. Starting with v0.1.3, each archive includes the README, license, and
security policy, including the supported execution boundary and
private vulnerability reporting instructions.
| System | Architecture | Archive suffix |
|---|---|---|
| Linux | amd64 / arm64 | linux_amd64.tar.gz / linux_arm64.tar.gz |
| macOS | Intel / Apple Silicon | macOS_amd64.tar.gz / macOS_arm64.tar.gz |
| Windows | x64 / ARM64 | windows_amd64.zip / windows_arm64.zip |
- Compute the archive's SHA-256 (
sha256sum FILEon Linux,shasum -a 256 FILEon macOS, orGet-FileHash FILE -Algorithm SHA256in PowerShell) and compare it with the matching filename inchecksums.txt. - Extract the archive. Keep its README and license; copy
starcli(orstarcli.exe) into a directory on your PATH. - Run
starcli --versionandstarcli -c 'print(6 * 7)'. The version must match the downloaded release and the script must print42.
Before upgrading, keep a copy of the currently installed binary and record its version. Stop running CLI processes, replace the binary, and repeat the checks above. To roll back, stop those processes and restore the saved binary. This rehearses binary replacement; backing up script-owned files or databases is the host application's responsibility.
The release workflow verifies all six archive layouts and checksums, then runs
installation, upgrade from v0.1.2, the CLI end-to-end suite, and binary rollback
on all six native targets. It publishes those same archives after the checks
pass and adds GitHub artifact attestations. For releases produced by this
workflow, verify provenance with gh attestation verify FILE --repo 1set/starcli.
Older releases may not include attestations. Binaries are not Apple notarized
or Windows Authenticode signed.
Clone the repository and build from source:
git clone https://github.com/1set/starcli.git
cd starcli
make buildBuild the static Linux binary before building the image. The image runs as UID 65532, includes system CA certificates, and uses the binary's embedded time-zone data. Arguments go directly to StarCLI, which also receives stop signals.
# Build and test the linux/amd64 image (also from an Apple Silicon host)
make build_linux
docker build --platform linux/amd64 -t starcli .
STARCLI_TEST_IMAGE=starcli go test ./e2e -run TestContainer -count=1
# Run in interactive mode
docker run --rm --platform linux/amd64 -it starcli
# Run a specific script
docker run --rm --platform linux/amd64 --read-only \
--cap-drop=ALL --security-opt=no-new-privileges \
--memory=256m --cpus=1 --pids-limit=64 \
--mount "type=bind,src=$(pwd),dst=/scripts,readonly" \
starcli --caps safe -I /scripts /scripts/your-script.star
# Serve a host-selected script; publish only on the host's loopback interface
docker run --rm --platform linux/amd64 --read-only \
--cap-drop=ALL --security-opt=no-new-privileges \
--memory=256m --cpus=1 --pids-limit=64 \
-p 127.0.0.1:8080:8080 starcli --caps safe \
--web-host 0.0.0.0 --web 8080 -c 'response.set_text("ready")'Mounted scripts must be readable by UID 65532. Mount a separate writable
directory when a trusted script needs persistent data. These examples are for
host-selected scripts; the image is not an untrusted multi-tenant execution
service. The container no longer uses /root, port 80, or a shell command as
its default entrypoint. Override --entrypoint explicitly if you need a shell.
$ ./starcli -h
Usage of ./starcli:
--allow-cmd allow ANY host command, gum subprocesses, and runtime environment mutation (trusted scripts only)
--allow-fs widen a restrictive tier with filesystem modules (file, path)
--allow-net widen a restrictive tier with network modules (http, net, email, llm)
--caps string capability tier: open (default net/fs) | full | network | safe; or env STAR_CAPS
--check syntax/resolve check the script (-c or file) without running it
-c, --code string Starlark code to execute
-C, --config string config file to load
--dangerously-allow-all DANGER: open everything β network + filesystem + host command execution of ANY command. Use only with fully trusted scripts.
-g, --globalreassign allow reassigning global variables in Starlark code (default true)
-I, --include string grant load() this directory (default: CWD only with filesystem capability)
-i, --interactive enter interactive mode after executing
-l, --log string log level: debug, info, warn, error, dpanic, panic, fatal (default "info")
--log-file string append the script's log module output to this file
--log-format string log file format: console (human) or json (machine) (default "console")
--max-output uint max top-level output entries per run (0=unlimited)
--max-steps uint max Starlark execution steps per run, guards runaway loops (0=unlimited)
-m, --module strings allowed modules to preload and load (default [args,atom,base64,cache,cmd,csv,email,emoji,file,go_idiomatic,gum,hashlib,http,json,liquid,llm,log,markdown,math,net,path,qrcode,random,re,regex,runtime,serial,sqlite,stats,string,struct,sys,time,toml,totp,web,yaml])
-o, --output string output printer: none,stdout,stderr,basic,lineno,since,auto (default "auto")
--record string record the complete session output (stdout+stderr) to this transcript file
-r, --recursion allow recursion in Starlark code
-V, --version print version & build information
-w, --web uint16 run web server on specified port, it provides request and response structs for Starlark code to useBy default StarCLI runs open, granting network and filesystem access.
Choose the capabilities needed by host-selected scripts with --caps (or the
STAR_CAPS env var). Module access follows this grant:
| tier | loadable modules |
|---|---|
(default) open |
network and filesystem; cmd loads with execution disabled; gum and runtime require --allow-cmd |
--caps full |
network and filesystem; cmd, gum, and runtime require --allow-cmd |
--caps network |
safe + network (http, net, email, llm) |
--caps safe |
pure / log / process only (math, json, sys, markdown, β¦) |
From a restrictive tier the granular flags widen the grant: --allow-net,
--allow-fs, and --allow-cmd. A module is classified by the union of
everything it can do, so the dual-capability modules β web (HTTP +
static_dir) and sqlite (DB + remote connect_remote) β need both
--allow-net and --allow-fs (or --caps full).
Local script imports follow a separate directory grant. The default open/full
posture and --allow-fs include the working directory. --caps safe and
--caps network do not implicitly expose it. An explicit -I directory grants
load() access to that directory without enabling the file module. Parent
traversal, absolute load names, and symlinks escaping the root are rejected;
symlinks must be relative and remain inside the root. This is an intentional
restriction compared with the previous os.DirFS behavior.
Host command execution (cmd) is the sharpest tool and is gated on its own.
The cmd module loads in the open posture, but run() is disabled β it
returns an error β until you pass --allow-cmd, which enables execution of any
command (no allowlist; still argv-only, never a shell). cmd is never granted
by a tier, not even full. For a one-flag "trust everything" run there is
--dangerously-allow-all β it opens network + filesystem + host
command execution of any command in a single switch. Use it only with fully
trusted scripts.
Set a stricter default for a whole deployment with the env var:
export STAR_CAPS=safe # default every invocation to the safe tierUnder a restrictive tier, a script that load()s a withheld module fails with a
non-zero exit code (4 for a withheld builtin). Execution budgets bound runaway
scripts: --max-steps caps Starlark computation steps and --max-output caps a
run's result size.
# open by default: network and filesystem modules load
$ ./starcli -c 'load("http", "get"); print(get)'
# sandbox down to safe: a network module is now withheld
$ ./starcli --caps safe -c 'load("http", "get")' # fails (exit 4)
# from safe, opt back into the network
$ ./starcli --caps safe --allow-net -c 'load("http", "get"); print(get)'
# host command execution needs its own explicit flag (then run() runs anything)
$ ./starcli --allow-cmd -c 'load("cmd", "run"); print(run("go version").stdout)'
# one-flag "trust everything": network + filesystem + run any command
$ ./starcli --dangerously-allow-all script.starStart an interactive REPL session:
$ ./starcliRun a single line of Starlark code:
$ ./starcli -c 'print("Hello, World!")'Run a Starlark script file:
$ ./starcli path/to/script.starExecute code and then enter interactive mode with the environment preserved:
$ ./starcli -c 'greeting = "Hello, World!"' -iStart a web server that executes Starlark code for HTTP requests:
$ ./starcli -w 8080 -c 'def handle_request(request): return {"message": "Hello from Starlark!"}'Run with debug-level logging:
$ ./starcli --log debug path/to/script.starThe args module is an argparse-style parser for the script's own arguments
(everything after --). argv[0] is the script name (or -c), like Python's
sys.argv; parse_args() parses argv[1:].
load("args", "ArgumentParser")
p = ArgumentParser(description = "greet someone")
p.add_argument("--name", default = "World", help = "who to greet")
p.add_argument("--count", type = int, default = 1)
p.add_argument("--shout", action = "store_true")
p.add_argument("file", help = "input file")
ns = p.parse_args()
print(ns.name, ns.count, ns.shout, ns.file)$ ./starcli greet.star -- --name Kevin --count 3 --shout in.txt
Kevin 3 True in.txtWhen a script uses the log module, --log-file routes all of its output to a
file at the interpreter level (the parent directory is created if needed, and
runs append):
load("log", "info", "warn")
info("starting up")
warn("careful now")$ ./starcli --log-file run.log job.star
$ cat run.log
2026-06-21T17:32:07.373+0800 info starting up
2026-06-21T17:32:07.373+0800 warn careful nowUse --log-format json for machine-readable logs (structured fields included):
$ ./starcli --log-file run.log --log-format json job.star
{"level":"info","ts":"2026-06-21T17:32:07.373+0800","msg":"starting up"}--record saves the complete session output β print output, results, REPL
interaction and errors β to a transcript file (appended, with a timestamped
session header), while still showing it live. Handy for replay and review.
REPL prompts and input echoes are included, so terminal transcripts can contain ANSI control sequences. Recording errors produce a non-zero exit status; an existing script error keeps its original exit code. If recording fails during execution, output continues to drain and the failure is reported at shutdown.
$ ./starcli --record session.log job.star # works in REPL mode too
$ cat session.log
===== starcli session 2026-06-21T17:43:24+08:00 =====
... everything the run printed (stdout + stderr) ...The sys module reads piped data from standard input (the script itself
still comes from a file or -c). sys.input() accepts a final line without a newline. All input helpers share
one buffered stream: consecutive calls and mixed input() / lines() / read()
consume the remaining data without discarding read-ahead. An empty input() at
EOF reports EOF. sys.read() returns all remaining stdin; sys.lines()
is a lazy iterator (a large stream is not buffered whole); sys.isatty()
tells interactive from piped input.
load("sys", "lines")
for line in lines():
print(line.upper())$ printf 'foo\nbar\n' | ./starcli upper.star
FOO
BARSyntax- and resolve-check a script without executing it (reports problems as
file:line:col: message, non-zero exit on any problem):
$ ./starcli --check path/to/script.star
$ ./starcli --check -c 'print(undefined_name)'
direct.star:1:7: undefined: undefined_name
check: 1 problem(s) foundStarCLI is the standard, fully-loaded build. It is assembled from a small
reusable core β the kit package β that you can use to build your own
CLI: a few-line Go shell that embeds your Starlark scripts and wires only the
modules you need. Your shell and the standard StarCLI construct their runtime
through the same path, so they behave identically.
//go:embed app.star
var app string
func main() {
// embed the script + pick modules + run, in one call
kit.Run(app, kit.WithModules("json", "math"))
}See examples/ for runnable demos (a minimal shell, and one wiring a
single starpkg module) and the build-your-own quickstart.
StarCLI can be configured through a config file (YAML format) using the -C or --config flag:
# Example config.yaml
host_name: MyStarCLIServer- Go 1.25.8 or later (prebuilt binaries do not require a Go installation)
# Build for current platform
make build
# Build for specific platforms
make build_linux
make build_mac
make build_windowsmake testThis project is licensed under the MIT License. See the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
For ordinary bugs, questions, or support, please open an issue on GitHub. For vulnerabilities, follow the private reporting instructions in SECURITY.md.
--web PORT serves host-selected, trusted scripts on 127.0.0.1 by default.
Use --web-host ADDRESS to explicitly allow another bind address. The defaults
are a 1 MiB body (--web-max-body), 16 admitted requests
(--web-max-concurrency), and a 15s execution deadline (--web-timeout). All
three limits must be positive. Oversized bodies return 413, read failures 400,
admission overflow 503, and execution deadlines 504; request cancellation
propagates to the interpreter. Transport limits are 16 KiB headers, 5s header
read, 10s request read, 30s idle, and execution deadline + 10s response write.
Ctrl-C / SIGTERM stop admission, cancel requests, and drain connections for up
to 5s before closing remaining connections. Startup errors return a nonzero
CLI status. Embedders can use web.StartContext for the same lifecycle.
These cooperative limits do not preempt arbitrary Go builtins or provide
process/RSS isolation. This entry point is for controlled scripts; untrusted
code and public multi-tenant execution require isolated workers and separate
admission review.
gum (including its editor, tmux and spinner paths) and runtime (including
putenv, setenv and unsetenv) require --allow-cmd or
--dangerously-allow-all. This applies to every tier, including the default
open tier, and to both preloaded modules and load() calls. --allow-net,
--allow-fs, and --caps full do not imply command execution. Use sys for
basic process/platform information without granting these capabilities.
--allow-cmd authorizes arbitrary host commands and their environment; it is
not a restricted command allowlist or an isolation boundary. The CLI supports
host-selected scripts. Run untrusted scripts in a separate constrained worker.