CLI reference
Full command reference for spens
Run a session
spens <environment> <agent> <workspace> [prompt] [options]| Argument or option | Description |
|---|---|
environment | Environment template name (e.g. node-24), or a path to a .json template file |
agent | Agent template name (e.g. pi), or a path to a .json template file |
workspace | Path to the directory to mount as the workspace |
prompt | Optional fourth argument. Runs the agent non-interactively (yolo mode): the agent executes the prompt and exits. The agent template must define a yolo_command |
--rebuild | Rebuild the Docker images from scratch, without cache |
--spens-dir PATH | Directory where session state is stored. Default: <workspace>/.spens |
--config PATH | Use a different spens config file for the session. Default: <workspace>/.spens.config.json. Relative paths resolve against the current directory. The file must exist when given |
--accept-changes | Auto-accept the agent's workspace changes when the session ends (skips the review prompt) |
--reject-changes | Roll the workspace back to its pre-session state after the agent exits |
--output MODE | Output mode: tty (default, pretty terminal output), jsonl (stream JSON events to stdout), or background (print the session id and exit; the session runs detached) |
--session-id ID | Use a specific session id instead of a random one. Must match ^[a-z0-9][a-z0-9-]{0,31}$; reusing an existing id is an error |
Notes:
jsonlandbackgroundmodes require a prompt and exactly one of--accept-changes/--reject-changes. Without an explicit decision, the end-of-session change review would block a non-interactive session forever.- Every session writes a machine-readable event stream to
<session_dir>/events.jsonland its current state to<session_dir>/state.json, regardless of output mode.
Session lifecycle commands
spens status <session-id> [--spens-dir PATH] # print session state and exit code
spens cancel <session-id> [--spens-dir PATH] # stop the session and mark it canceled
spens attach <session-id> [--spens-dir PATH] # follow the session event stream until it endsThese work for every spens session, regardless of output mode. Terminal states are finished, canceled, and error.
spens init
spens init [options]| Option | Description |
|---|---|
--yes, -y | Skip all prompts and use defaults for unanswered questions. Requires --stack and --provider |
--stack | Stack used: node, python, dotnet, rust, go, java, manual |
--provider | Model provider(s): fireworks, anthropic, openrouter, openai, manual. Comma-separated for multiple (e.g. openai,fireworks) |
--public-get / --no-public-get | Allow or deny public GET access (default: deny) |
--tooling-urls / --no-tooling-urls | Allow or deny the stack's common tooling URLs (default: allow) |
--inject-env / --no-inject-env | Configure the provider's common env vars: inject secrets for approved domains and forward non-secret vars (default: allow) |
--output PATH | Write the config somewhere other than .spens.config.json |
--force | Overwrite an existing config without asking |
--dry-run | Print the generated config to stdout without writing any file |
spens list
spens listPrints all available environments and agents, including local templates from the workspace templates/ directory.
spens log-viewer
spens log-viewer [workspace] [options]| Option | Description |
|---|---|
--port PORT | Port for the web server (default: 7331) |
--host HOST | Host to bind (default: 127.0.0.1) |
--no-browser | Do not open a browser automatically |
--spens-dir PATH | Read sessions from a specific spens directory instead of searching for .spens near the workspace |
spens pricing
spens pricing [options]| Option | Description |
|---|---|
--refresh | Re-fetch every provider's pricing dataset into the local cache |
--vendor | Re-fetch and also update the snapshots shipped in spens/data/pricing (needs a writable checkout) |
--provider NAME | Limit to one provider (repeatable). Providers: anthropic, openai, google, fireworks-ai, openrouter, bedrock |
--model ID | Show the resolved price for one model id |
--tokens N | Context size used to pick a banded model's price with --model |
--offline | Do not fetch; report what cached and vendored data provide |
Spens config file
Spens reads an optional .spens.config.json from the workspace root. spens init generates one for you; you can also write it by hand. To use a different config file for a single session, pass --config PATH (e.g. spens node-24 pi . --config .spens.config.ocr.json).
A full example:
{
"egress": "enforced",
"nono_override": "profile_override.json",
"additional_capture_urls": ["*opencode.ai*"],
"exclude_capture_urls": ["*models.opencode.ai*"],
"env": ["OPENAI_BASE_URL"],
"pre_sandbox_commands": ["npm install"],
"domain_rules": [
{"pattern": "*api.github.com*", "allow": ["GET"]},
{"pattern": "*pypi.org*", "allow": ["*"]},
{"pattern": "*api.anthropic.com*", "allow": ["GET", "POST"]}
],
"inject_headers": [
{
"placeholder": "ANTHROPIC_API_KEY",
"env_var": "ANTHROPIC_API_KEY",
"for_domains": ["*api.anthropic.com*"]
}
],
"pricing": {"live_fetch": false}
}Field reference
| Field | Description |
|---|---|
egress | "enforced" (default) or "legacy". Enforced: the agent runs on an isolated internal Docker network whose only neighbor is the interceptor, so all egress, including DNS, is forced through it and non-proxied protocols fail closed. Legacy: the agent shares the interceptor's network namespace and proxy env vars are advisory only. Not recommended |
nono_override | Path to a custom nono profile JSON file, replacing the auto-generated combined profile. The generated profile is always preserved inside the image as spens-base.json, so your override can use "extends": ["spens-base"] to layer your own rules on top of Spens internals |
additional_capture_urls | Glob-style URL patterns to additionally capture in the interceptor |
exclude_capture_urls | Glob-style URL patterns to skip in the interceptor |
env | Environment variable names to forward from the host to the agent container with their real values. Use for non-secret variables only |
pre_sandbox_commands | Shell commands to run inside the container before the sandbox starts the agent (e.g. npm install). They run unsandboxed as the unprivileged agent user |
domain_rules | Per-domain method restrictions. Each rule has a glob pattern (matched against the request hostname only) and an allow list of HTTP methods, or ["*"] for all methods. Fails closed once any rule is configured |
inject_headers | Secret substitution rules. See below |
pricing | Costing options. {"live_fetch": false} stops spens fetching the pricing dataset at the end of a session |
Extending the generated nono profile
When you use nono_override, the generated profile is replaced at runtime. The generated profile is still preserved inside the image as spens-base.json, so your override can extend it rather than recreating all the Spens internals. For example, a profile_override.json that only adds an extra allowed folder:
{
"extends": ["spens-base"],
"allow_folders": ["/tmp/custom-cache"]
}This inherits the proxy environment variables, cache sanitization, placeholder injection, and any base-pack extensions that Spens generates, and layers your own rules on top.
How inject_headers works
inject_headers rules keep real secrets out of the agent container. Each rule has three parts:
placeholder— the literal string the agent is given as its "key"env_var— the host environment variable that holds the real secretfor_domains— the hostnames the secret may be substituted on
A worked example. Suppose your host environment has ANTHROPIC_API_KEY=sk-ant-secret123 and your config contains the rule from the example above. At runtime:
- The agent container is started with
ANTHROPIC_API_KEY=ANTHROPIC_API_KEY. The variable name is real, but its value is the literal placeholder string. The agent reads it and sends it in its API requests, but it holds no secret. - The interceptor container is started with the real value of
ANTHROPIC_API_KEYfrom your host. - When the agent sends a request whose hostname matches
for_domains(here,api.anthropic.com), the interceptor replaces every occurrence of the placeholder string with the real secret before forwarding the request upstream. - On any other hostname, the placeholder is left untouched, so the real secret can never be attached to a request to a destination you did not authorize.
- Real secret values are masked out of the request log and trace files before anything is written to disk.
placeholder and env_var do not have to share a name. If your host stores the secret under ANTHROPIC_KEY, write "placeholder": "ANTHROPIC_API_KEY", "env_var": "ANTHROPIC_KEY" — the agent still sees ANTHROPIC_API_KEY, and the interceptor reads the real value from ANTHROPIC_KEY.
A rule without for_domains (or with an empty list) authorizes no hostnames, so its placeholder is never substituted.
Default network policy
Two mechanisms control the agent's network access, and they behave differently by default:
- Non-HTTP protocols always fail closed. The agent's Docker network has no route to the internet except through the interceptor, and the interceptor refuses tunnels that do not speak HTTP. SSH, raw sockets, and other non-HTTP protocols cannot get out, regardless of configuration.
- HTTP/HTTPS filtering depends on
domain_rules. Ifdomain_rulesis omitted or empty, no policy is enforced and every domain is denied (fail closed). Traffic is still routed through the interceptor, so any request that is allowed by an explicit rule is captured and logged. Once any rule is configured, the policy continues to fail closed: a hostname that matches no rule is blocked, and HTTPS tunnels to unmatched hosts are refused before they are established.
If you want allow-by-default HTTP egress, add a wildcard rule such as {"pattern": "*", "allow": ["*"]} to domain_rules. spens init generates a ruleset for your stack and provider; answering manual for both the stack and the provider produces an empty ruleset, which means deny-all.
Spens