CLI Reference
The CLI is available as nsys-cortex after linking (npm link -w packages/cortex-cli), or via npx nsys-cortex / npm run cortex. The nsys-cortex launcher executes the compiled bundle (dist/index.js, produced by npm run build) on plain node; if the bundle hasn't been built it falls back to running the TypeScript source through tsx. See Development for the build details.
All commands accept a global --mode <auto|dev|docker|firecracker> flag (default: auto — docker when a Docker daemon and the neuron image are present, otherwise dev; firecracker is never chosen automatically; config runs without resolving a mode). docker runs the agent in a hardened container (needs Docker + npm run build:image; see Configuration → Docker mode); firecracker is the MicroVM boundary (Linux + KVM + root).
Dev mode requires explicit opt-in. Set NSYS_CORTEX_ALLOW_INSECURE_DEV=1 before running any subcommand in dev mode — the CLI refuses otherwise. Dev mode is not a security boundary; the agent runs as your user. See Configuration → Security for details.
On first run, ~/.nsys-cortex/config.json is auto-created with default settings. Edit it to set API keys and defaults — see Configuration for details.
The exec, install, shell, and chat commands auto-create the neuron if it is not already running. If a neuron exists from another process that has since exited, it is restarted in the current process; if that process is still alive, the command refuses with its pid (Error: Neuron <id> is running under another process (pid N, …), exit 1) — send commands through that process (the HTTP API server) or run nsys-cortex destroy <id> first.
The ASCII banner is shown for interactive commands (chat, shell), --help, and all subcommand help pages (<cmd> --help, help <cmd>). Non-interactive commands output clean results without the banner.
Installing the CLI itself is the README's "Install" section: the release-asset scripts scripts/install/install.sh (Linux, macOS) and scripts/install/install.ps1 (Windows) run npm install -g @nsys-cortex/cli@<pinned version> with your own Node 24, verify nsys-cortex --version and report whether Docker is present; the install command below installs packages inside a neuron.
Commands
nsys-cortex --version Show version
nsys-cortex --help Show help and list all commands
nsys-cortex create <agent-id> Create a new neuron sandbox
nsys-cortex exec <agent-id> <command...> Execute command in neuron (-l/--language bash|node|python)
nsys-cortex install <agent-id> <pkg> Install package (--via apt|npm|pip)
nsys-cortex snapshot <agent-id> Snapshot neuron (refused outside Firecracker mode, exit 1; not yet supported under the jailer either)
nsys-cortex destroy <agent-id> Destroy neuron (--wipe to delete overlay; exit 1 for an unknown id; says whether the overlay was kept)
nsys-cortex list List neurons (running + stopped, includes cross-process)
nsys-cortex shell <agent-id> Interactive shell (exit with Ctrl+D or "exit")
nsys-cortex chat <agent-id> AI chat session (markdown output, persistent history)
nsys-cortex config <get|set|unset|path> Inspect and edit ~/.nsys-cortex/config.json
nsys-cortex test-persistence <agent-id> Run automated persistence test
exec exits with the exit code of the command it ran inside the neuron (1 when the neuron could not be reached) and writes only that command's stdout to stdout — its own status lines (Starting neuron …, Exit code: N (Tms), Error: …) go to stderr — so nsys-cortex exec id 'test -f x' && … and out=$(nsys-cortex exec id cat f) work.
<agent-id> names one directory under the overlay store, so it must be a single path component: no /, not . or .., and no whitespace or control characters (the HTTP API is stricter still: [A-Za-z0-9_-], at most 64). Anything else is refused before the filesystem is touched, and an existing overlay directory with such a name (created before this rule) is ignored by list with a warning — rename it (mv 'foo bar' foo-bar) to keep using it.
Config Command
config reads and edits ~/.nsys-cortex/config.json without hand-editing JSON — the friendly path to setting API keys and defaults.
nsys-cortex config get # whole config (secrets redacted)
nsys-cortex config get llm.defaultProvider # one value by dotted path
nsys-cortex config set llm.providers.anthropic.apiKey sk-ant-... # set an API key
nsys-cortex config set llm.defaultProvider openai # switch active provider
nsys-cortex config unset llm.providers.grok.apiKey # remove a value
nsys-cortex config path # print the file location
- Value coercion —
setcoerces the argument:"true"/"false"→ boolean, a bare number → number, JSON ([...]/{...}) → parsed, everything else stays a string. Soconfig set port 3200stores a number whileconfig set ...apiKey sk-...stays a string. - Validation —
set/unsetvalidate the resulting config against the schema before writing; an edit that would make the config invalid is rejected with the offending path, and nothing is saved. - Typo guard — creating a brand-new config section (e.g. a misspelled
llm.provders.…) still writes (custom keys are allowed) but prints a⚠ Created new config section(s)warning so typos don't fail silently. - Secrets — values on
apiKey/token/secret/passwordpaths are redacted ingetoutput; pass--secretsto reveal them. The config file is written mode0o600(it holds plaintext API keys) via an atomic tmp+rename.
Install Command
The install command installs packages inside the neuron (in dev mode the neuron is the host, so there it installs on the host). Packages persist across neuron restarts via the overlay filesystem. The CLI and the HTTP API build the same command (buildInstallCommand() in @nsys-cortex/host), so what one refuses the other refuses too: the package name must match [@a-z0-9][a-z0-9._/-]{0,213} (no whitespace, no shell metacharacters, no version specifier) and is single-quoted into the command after --.
| Manager | dev mode (the host) | docker and firecracker mode (the Alpine neuron image) | Install location |
|---|---|---|---|
npm (default) |
npm install -- '<pkg>' |
npm install -- '<pkg>' |
<workspace>/node_modules/ |
pip |
pip3 install -- '<pkg>' |
pip3 install --user --break-system-packages -- '<pkg>' — the image's Python is PEP 668-managed and its system site-packages unwritable |
the host's Python / /home/agent/.local (persists in the overlay's home) |
apt |
apt-get install -y -- '<pkg>' |
refused — the image is Alpine Linux (apk), non-root, with a read-only root; use npm or pip |
the host's packages |
A refusal (apt outside dev mode, an unknown --via, a malformed name) prints the reason on stderr and exits 1 before any neuron starts. A failed install shows the neuron's stderr, then Failed to install <pkg> via <via>: exit code N, and exits 1; success prints Package <pkg> installed successfully (Tms). Status lines go to stderr; the package manager's own stdout stays on stdout (truncated at 2000 bytes).
Shell
The shell command opens an interactive shell inside a neuron.
The greeting shows the workspace path:
- Dev mode: Spawns
bashdirectly with full PTY passthrough viastdio: 'inherit'. Shows the host overlay path (e.g.,~/.nsys-cortex/overlays/my-agent) with a note that paths are on the host filesystem. - Docker and Firecracker modes: Allocates a real PTY inside the neuron via
node-pty, with I/O streamed over vsock. Interactive programs (vim,nano,less,top), arrow keys, tab completion, and Ctrl+C all work. Terminal resize events (SIGWINCH) are forwarded to keep the PTY in sync with the host terminal. Shows/home/agent/workspace.
Requires node-pty in the neuron image (docker mode — npm run build:image compiles it) or in the VM rootfs (Firecracker mode). If node-pty is unavailable, pty_spawn returns a clear error and the shell exits 1. A PTY command that fails mid-session ends the shell with the agent's error on stderr (PTY read failed: …, Shell connection lost: …) and exit code 1; "Shell closed" with exit 0 means the shell inside the neuron ended (exit or Ctrl+D).
Chat Mode
The chat command supports both interactive and non-interactive modes:
- Interactive (default when TTY is attached): readline-based prompt with
/help,/exit,/clear,/model,/cost,/save,/sessions,/contextcommands. LLM responses are rendered as styled markdown (bold, headers, code blocks, tables, lists). Per-neuron input history is persisted at~/.nsys-cortex/chat-history/<agentId>— use arrow up/down to recall previous messages across sessions. Full conversation transcripts persist to~/.nsys-cortex/sessions/and can be resumed with--continue/--session(see Session Persistence & Resume). Contextual suggestion hints appear as dim ghost text after each AI response (see below). - Non-interactive: triggered by
--message/-pflag or piped stdin. When stdout is a TTY, output is rendered as styled markdown. When piped (e.g.,| cat,> file), output is raw text without ANSI codes for composability. Use--jsonfor structured output.
Options
| Option | Description |
|---|---|
-m, --model <model> |
LLM model to use |
-p, --message <msg> |
Send a single message and exit (non-interactive) |
--json |
Output result as JSON object (non-interactive only) |
--admin |
Enable the full tool preset (bash, process_*, elevated). Off by default. |
-c, --continue |
Resume the most recent saved session for this agent |
--session <id> |
Resume a specific session by id (must belong to this agent) |
--output-schema <file> |
Constrain the final response to a JSON Schema file (structured outputs; non-interactive only) |
--image <file> |
Attach an image to the message (repeatable; png/jpg/gif/webp, max 5MB each; non-interactive) |
--think [budget] |
Enable extended thinking / reasoning. Optional integer budget in tokens (default 4000). Anthropic, OpenAI, Gemini. |
--approve <classes> |
Non-interactive only: grant approval requests for these effect classes without asking (comma-separated, or all); every other request is denied (ADR-012, #16). |
In interactive chat, attach images inline by referencing them as @path/to/image.png in your message — existing image files are loaded and attached (the @token stays in the text so the model knows which attachment is which); non-existent paths are treated as plain prose. Vision requires a vision-capable model — text-only local models will ignore or garble image input.
By default, chat exposes the coding preset: read, write, edit and bash (the shell tool, under the standard command policy). Pass --admin to also expose process_start/list/read/kill, apply_patch, elevated, the memory and session tools, etc. Note that --admin is not what grants shell access — coding already has it; in dev mode (not a security boundary) a prompt-injected agent can run commands as your user under either preset. readonly is the preset without bash.
If MCP servers are configured (mcpServers in config.json), their tools load at chat startup on top of the active preset. With a few tools the greeting shows [mcp] N external tool(s) loaded; above the defer threshold (default 10) it shows [mcp] N external tool(s) available on demand via mcp_find_tools — the agent then loads only the tools it actually needs, saving the context their schemas would otherwise cost on every turn. A server that fails to connect is skipped with a logged warning; the chat starts regardless. Only connect MCP servers you trust: their tools run outside the neuron sandbox. See Agent Toolkit → MCP Integration.
Extended thinking
Anthropic, OpenAI, and Gemini all expose a "thinking" or "reasoning" mode where the model spends extra tokens reasoning before answering. --think enables it:
nsys-cortex chat my-agent --think # 4000-token thinking budget (default)
nsys-cortex chat my-agent --think 8000 # explicit 8000-token budget
nsys-cortex chat my-agent -p "plan this" --think
The interactive chat renders streaming thinking as dim italic with a [thinking] gutter before the response:
You> Plan a path through this codebase
Cortex>
[thinking]
I should look at the package structure first, then trace the entry point…
Here's the plan: …
Per-provider behavior:
| Provider | Mapping | Reasoning text streamed? | Usage bucket |
|---|---|---|---|
| Anthropic (Claude 4.x) | thinking: { budget_tokens: N } |
Yes — full text + HMAC signature | Folded into outputTokens |
| OpenAI (o-series, GPT-5) | reasoning_effort: low / medium / high (bucketed) |
Only summaries (GPT-5) | reasoningTokens |
| Gemini (2.5+, 3.x) | thinkingConfig.thinkingBudget: N + includeThoughts |
Yes — thought summaries | reasoningTokens |
| Grok (xAI) | Inherits OpenAI Chat Completions wiring | Provider-dependent | reasoningTokens if reported |
| Ollama | reasoning_effort (bucketed; xhigh on Qwen3.8) — honoured by the loaded model or ignored |
reasoning_content deltas when the server's reasoning parser emits them |
reasoningTokens if reported |
| vLLM | reasoning_effort (bucketed; xhigh on Qwen3.8) — honoured by the loaded model or ignored |
reasoning_content deltas when the server's reasoning parser emits them |
reasoningTokens if reported |
| LM Studio | reasoning_effort (bucketed; xhigh on Qwen3.8) — honoured by the loaded model or ignored |
reasoning_content deltas when the server's reasoning parser emits them |
reasoningTokens if reported |
| SGLang | reasoning_effort (bucketed; xhigh on Qwen3.8) — honoured by the loaded model or ignored |
reasoning_content deltas when the server's reasoning parser emits them |
reasoningTokens if reported |
| Mistral | Magistral uses a non-OpenAI-compat surface — dropped | n/a | n/a |
Budget-to-effort mapping for OpenAI / Grok (Chat Completions has no numeric knob): ≤1024 → low, ≤4096 → medium, otherwise high — xhigh instead of high on Qwen3.8, whose chat template rejects high. Without --think nothing is sent and the server's default effort applies (the KB's vLLM box sets low).
Constraints (intentional):
- Tokens are billed separately. Anthropic counts thinking against
output_tokensdirectly; OpenAI and Gemini surface a separatereasoningTokensfield on the usage object so cost reporting can break it out. - Temperature interactions. Anthropic forces
temperature=1while thinking is on; OpenAI reasoning models ignore (and may reject)temperatureso the provider drops it; Gemini accepts any temperature with thinking enabled. - OpenAI hides the raw chain-of-thought. What you see in the
[thinking]gutter on GPT-5 is the brief summary the API exposes viareasoning_content— not the model's internal reasoning. The actual CoT lives only in OpenAI's servers.
When tool use is involved, the agent loop preserves Anthropic's HMAC signature on every thinking block across turns. Without that round-trip, subsequent tool-use turns 400 with thinking blocks must be preserved in subsequent turns. OpenAI and Gemini don't sign their reasoning blocks; the agent loop still tracks them for in-run continuity but they're cheap to drop. This is handled transparently — you don't need to think about it (no pun intended), but it's why thinking blocks live in the session transcript as structured JSON content rather than raw text.
Non-Interactive Examples
# Single message, text output
nsys-cortex chat my-agent -p "What is 2+2?"
# Single message, JSON output
nsys-cortex chat my-agent -p "List files" --json
# Piped input
echo "Generate a Dockerfile" | nsys-cortex chat my-agent
# File as input
cat prompt.txt | nsys-cortex chat my-agent
# Capture output in a variable
RESULT=$(nsys-cortex chat my-agent -p "What files are in /tmp?")
# Pipe output to a file
nsys-cortex chat my-agent -p "Generate a Dockerfile" > Dockerfile
# Structured output: response conforms to a JSON Schema file
# (see docs/agent-runtime.md#structured-outputs for per-provider behavior;
# note the chat tool preset counts as "tools present" on Anthropic/Gemini)
nsys-cortex chat my-agent -p "Summarize the repo" --output-schema summary.schema.json | jq .
# Vision input: attach images (repeatable flag)
nsys-cortex chat my-agent -p "what changed between these screenshots?" \
--image before.png --image after.png
# Interactive: reference image files inline with @
# You> compare @designs/mock.png with @screenshot.png
JSON Output Format
When --json is used, the output is a single JSON object:
{
"agentId": "my-agent",
"sessionId": "...",
"model": "claude-opus-5-5",
"response": "The answer is 4.",
"totalTurns": 1,
"usage": { "inputTokens": 412, "outputTokens": 89 },
"toolCalls": [
{ "name": "bash", "input": { "command": "echo $((2+2))" }, "durationMs": 150 }
]
}
Approvals
A tool whose effect class agent.approval.requireFor lists (default destructive; NSYS_CORTEX_APPROVAL_REQUIRE_FOR), a tool whose policy requires approval, or elevated at the ask level runs only after an explicit approval (ADR-012, #16). In interactive chat the request is asked at the prompt — ⚠ approval: <tool> (<class>) <input> — allow? [y/N] — y runs it, anything else (Enter included) denies it, and the model is told denied: <reason>; the policy's timeout (NSYS_CORTEX_APPROVAL_TIMEOUT_MS, 300 s) and Ctrl-C end the wait as a deny. Non-interactive chat (-p, piped stdin) denies every request unless --approve <class[,class]> or --approve all grants its class — a pipe never approves by accident; a denied request is noted on stderr.
Interactive Chat Commands
During an interactive chat session, the following commands are available:
| Command | Description |
|---|---|
/help |
List all chat commands |
/exit |
End the chat session and return to the terminal |
/quit |
Alias for /exit |
/clear |
Reset the session — starts a fresh session, discards history |
/model [name] |
Show the current model + registered models, or switch mid-session |
/cost |
Cumulative token usage for the session lifetime (input/output/cache/reasoning; survives --continue) |
/save [path] |
Export the transcript as markdown (default ./cortex-chat-<id>-<date>.md) |
/sessions |
List saved sessions for this agent (marks the current one) |
/context |
Display context window usage (tokens, budget, compaction) |
↑ / ↓ |
Browse input history (persisted per neuron across sessions) |
Ctrl-C |
During a response: cancel the in-flight run. At the prompt: exit |
An unknown /word (/bogus, /bogus with args) prints Unknown command /bogus — type /help for the list and costs no model turn; a message that starts with a path (/etc/hosts tells what?) is sent as typed, since the slash-word is followed by /, not a space. Non-interactive mode (-p, piped stdin) sends every line as a message, as before.
/model <name> takes effect on the next message — switching mid-session keeps the conversation history, so you can draft with a cheap local model and switch to a flagship for the final pass. Names not registered in the context-budget table are passed to the provider verbatim (with a 128k default budget), which is how operator-chosen Ollama/vLLM/LM Studio model names work. /cost reports the per-bucket token counts the providers billed, accumulated over the session's whole lifetime — it's read from the persisted session usage, so resuming with --continue keeps the running total rather than resetting; dollar cost depends on each provider's pricing. /save exports everything the faithful transcript stores — user/assistant text, tool calls with inputs, abbreviated tool results, and thinking blocks — as a readable markdown file (mode 0o600).
While the agent is working, a spinner (⠋ thinking… / ⠹ running…) shows activity — responses are buffered and rendered as markdown once complete, so the spinner is what tells you it's still going. Ctrl-C mid-response aborts the in-flight request and any running tool (cancelled cleanly, not killed) and returns you to the prompt with the session intact; Ctrl-C at an idle prompt exits. The spinner is suppressed on non-TTY output so piped/redirected runs stay clean.
Session Persistence & Resume
Chat transcripts are persisted to ~/.nsys-cortex/sessions/ (per-agent JSONL, mode 0o600; override the location with NSYS_CORTEX_SESSION_STORAGE_DIR), so conversations survive across CLI invocations and machine restarts.
nsys-cortex chat my-agent # start a fresh session
nsys-cortex chat my-agent --continue # resume the most recent session for this agent
nsys-cortex chat my-agent -c # short form of --continue
nsys-cortex chat my-agent --session ses-abc # resume a specific session by id
On resume, the last few turns are replayed so you have context for where you left off. Use /sessions inside a chat to list this agent's saved sessions and copy an id for --session.
Persistence is faithful: tool calls, their results, and attached images are stored and reconstructed on resume (not just the assistant's prose), so a resumed agent still "remembers" the files it read, commands it ran, and screenshots it saw. Note that image attachments are stored base64-inline in the JSONL transcript, so sessions with many images grow the storage dir accordingly (the 24h maxAge sweep bounds this over time). Extended-thinking blocks are recorded for transcript fidelity but are not replayed back to the model on reload — a provider's reasoning signature is bound to its original request and the tool-use/result pairs already carry the actions. Expired sessions (older than the 24h maxAge, configurable via NSYS_CORTEX_SESSION_MAX_AGE) are skipped and cleaned up on load.
Suggestion Hints
After each AI response, dim "ghost text" may appear after the You> prompt with a contextual next-step suggestion:
You> run build and tests ← dim ghost text
| Key | Action |
|---|---|
→ (right arrow) |
Accept suggestion — fills it into the input |
Tab |
Cycle through available suggestions (up to 3) |
| Any other key | Clear ghost text, proceed with normal typing |
Suggestions are generated heuristically based on the agent's tool calls and response:
| Condition | Suggestion |
|---|---|
| Any tool had a non-zero exit code | fix the error |
| Tests ran and failed | fix failing tests |
| Tests ran and passed | run tests again |
| Files were written or edited | run build and tests |
| Response mentions "created", "added", "implemented" | review the changes |
git diff or git status was run |
commit the changes |
| Response mentions "installed" or "install" | verify installation |
| Fallback | what's next? |
If you backspace to an empty prompt, the suggestion reappears. Suggestions matching your last input are suppressed to avoid repetition (e.g., after you say "verify installation", it won't suggest it again).
/context output
The /context command shows a summary of the current context window state:
Context Window Usage
────────────────────────────────────────
Model: claude-opus-5-5
Context window: 1,000,000 tokens
Output reserve: 8,192 tokens
Usable budget: 991,808 tokens
────────────────────────────────────────
System prompt: 3,412 tokens
Conversation: 1,280 tokens (6 messages)
Total used: 4,692 / 991,808 tokens (0.5%)
────────────────────────────────────────
Compaction at: 85% usage
(Above example uses Opus 4.8's 1M context. Older / 200k-tier models like Opus 4.6 or Haiku 4.5 will show 200,000 tokens and a 191,808 usable budget instead — see Budget Defaults for the per-model table.)
When usage exceeds the compaction threshold, a warning is shown:
⚠ Compaction will trigger on next message
Examples
# Show version and help
nsys-cortex --version
nsys-cortex --help
# Neuron lifecycle
nsys-cortex create my-neuron
nsys-cortex exec my-neuron "ls -la"
nsys-cortex exec my-neuron "console.log(1+1)" -l node
nsys-cortex install my-neuron lodash --via npm
nsys-cortex destroy my-neuron --wipe
# Persistence test
nsys-cortex test-persistence my-neuron
# Interactive AI chat (requires ANTHROPIC_API_KEY or OPENAI_API_KEY)
nsys-cortex chat my-neuron
nsys-cortex chat my-neuron --model claude-opus-5-5
# Non-interactive chat
nsys-cortex chat my-neuron -p "What is 2+2?"
nsys-cortex chat my-neuron -p "List files" --json
echo "Hello" | nsys-cortex chat my-neuron
# During an interactive chat session:
# You> /help — list all commands
# You> /model gpt-6.1-sol — switch model mid-session
# You> /cost — cumulative token usage
# You> /save — export transcript as markdown
# You> /context — show token usage
# You> /clear — reset conversation
# You> /exit — quit chat