Spens logoSpens

CLI reference

Full command reference for spens

Run a session

spens <environment> <agent> <workspace> [prompt] [options]
Argument or optionDescription
environmentEnvironment template name (e.g. node-24), or a path to a .json template file
agentAgent template name (e.g. pi), or a path to a .json template file
workspacePath to the directory to mount as the workspace
promptOptional fourth argument. Runs the agent non-interactively (yolo mode): the agent executes the prompt and exits. The agent template must define a yolo_command
--rebuildRebuild the Docker images from scratch, without cache
--spens-dir PATHDirectory where session state is stored. Default: <workspace>/.spens
--config PATHUse 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-changesAuto-accept the agent's workspace changes when the session ends (skips the review prompt)
--reject-changesRoll the workspace back to its pre-session state after the agent exits
--output MODEOutput 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 IDUse 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:

  • jsonl and background modes 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.jsonl and 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 ends

These work for every spens session, regardless of output mode. Terminal states are finished, canceled, and error.

spens init

spens init [options]
OptionDescription
--yes, -ySkip all prompts and use defaults for unanswered questions. Requires --stack and --provider
--stackStack used: node, python, dotnet, rust, go, java, manual
--providerModel provider(s): fireworks, anthropic, openrouter, openai, manual. Comma-separated for multiple (e.g. openai,fireworks)
--public-get / --no-public-getAllow or deny public GET access (default: deny)
--tooling-urls / --no-tooling-urlsAllow or deny the stack's common tooling URLs (default: allow)
--inject-env / --no-inject-envConfigure the provider's common env vars: inject secrets for approved domains and forward non-secret vars (default: allow)
--output PATHWrite the config somewhere other than .spens.config.json
--forceOverwrite an existing config without asking
--dry-runPrint the generated config to stdout without writing any file

spens list

spens list

Prints all available environments and agents, including local templates from the workspace templates/ directory.

spens log-viewer

spens log-viewer [workspace] [options]
OptionDescription
--port PORTPort for the web server (default: 7331)
--host HOSTHost to bind (default: 127.0.0.1)
--no-browserDo not open a browser automatically
--spens-dir PATHRead sessions from a specific spens directory instead of searching for .spens near the workspace

spens pricing

spens pricing [options]
OptionDescription
--refreshRe-fetch every provider's pricing dataset into the local cache
--vendorRe-fetch and also update the snapshots shipped in spens/data/pricing (needs a writable checkout)
--provider NAMELimit to one provider (repeatable). Providers: anthropic, openai, google, fireworks-ai, openrouter, bedrock
--model IDShow the resolved price for one model id
--tokens NContext size used to pick a banded model's price with --model
--offlineDo 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

FieldDescription
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_overridePath 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_urlsGlob-style URL patterns to additionally capture in the interceptor
exclude_capture_urlsGlob-style URL patterns to skip in the interceptor
envEnvironment variable names to forward from the host to the agent container with their real values. Use for non-secret variables only
pre_sandbox_commandsShell 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_rulesPer-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_headersSecret substitution rules. See below
pricingCosting 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 secret
  • for_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:

  1. 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.
  2. The interceptor container is started with the real value of ANTHROPIC_API_KEY from your host.
  3. 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.
  4. 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.
  5. 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. If domain_rules is 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.

On this page