Agentic coding in your terminal — powered by Axon models by MatterAI.
📖 Full documentation: https://docs.matterai.so/orbcode-cli/overview
OrbCode CLI is a standalone terminal port of the Orbital extension: the same Axon models, the same native tool schemas, the same MatterAI auth backend — rebuilt from scratch as an interactive TUI with streaming chat, live thinking display, tool activity rows, edit/command approvals, and todo tracking.
- Quick start
- Install
- Updating / relinking
- Usage
- Authentication
- Models
- The TUI
- Slash commands
- Keyboard shortcuts
- Approvals & safety
- Headless mode
- Configuration
- Hooks
- Architecture
- Tools
- Agent loop
- Development
- Tests
- Troubleshooting
- Documentation
- Contributing
- License
npm install -g @matterailab/orbcode # install the CLI (needs Node.js >= 20)
orbcode login # sign in once via your browser
cd your-project && orbcode # start codingThat's it — you're in an interactive agent session. Ask it to read, edit, run commands, or fix bugs. New here? Skim the docs or jump to Usage.
Prefer not to install globally? Run it on the fly:
npx @matterailab/orbcodeRequires Node.js >= 20.
npm install -g @matterailab/orbcodeThen, from any project directory:
orbcodeTo update later: npm update -g @matterailab/orbcode (or re-run the install command).
git clone https://github.com/MatterAIOrg/OrbCode.git
cd OrbCode
npm install
npm run build
npm link # exposes the global `orbcode` commandnpm link creates a symlink to this repo, so after pulling changes you only
need to rebuild — no relink required:
npm run build # the linked `orbcode` command picks this up immediatelyRelink only when the package name or bin entry changes (e.g. the package was
renamed orbitalcode → orbcode):
npm unlink -g orbitalcode # remove a stale link under the old name (once)
npm linkThe version reported by orbcode --version is read from package.json at
runtime — bumping the version there is all that's needed.
orbcode start an interactive session in the current directory
orbcode "<prompt>" start an interactive session with an initial prompt
orbcode login sign in to MatterAI (browser device flow)
orbcode -p "<prompt>" run a single prompt non-interactively, print only the final response
orbcode -p "…" --yolo non-interactive with edits/commands auto-approved
orbcode --model <id> use a specific model for this run (also -m)
orbcode --resume <id> resume a previous session by id (also -r)
orbcode --version print version
orbcode --help show help
The TUI always takes over the full terminal screen on launch (prior shell output stays in scrollback).
The directory you launch from becomes the workspace directory: the default target for file operations and commands, and the file listing the model sees in its environment details.
Browser-based device flow with polling (no copy/paste needed):
orbcode login(or/loginin the TUI) callsPOST /orbcode/auth/starton the MatterAI backend, which issues a one-time 48-hex device code (10-minute lifetime, stored in redis).- The CLI opens
https://app.matterai.so/orbital?loginType=orbcode&devicecode=<code>in your browser:- Already signed in → the webapp shows the Authorize OrbCode CLI dialog immediately.
- Not signed in → you're redirected to sign-in first. The
devicecodequery param is preserved through the OAuth state (Google/Microsoft) and the email/password path, so the authorize dialog appears right after sign-in.
- Clicking Authorize binds your session token to the device code
(
POST /orbcode/auth/authorize). - Meanwhile the CLI polls
GET /orbcode/auth/poll?devicecode=…(every 3s by default, bounded by the code's lifetime). The token is handed out exactly once — the redis key is deleted on first successful poll. - The CLI verifies the token against the profile endpoint and saves it.
Fallbacks & overrides:
- settings.json key: set
apiKeyin~/.orbcode/settings.json(or a project's.orbcode/settings.json) to skip login. The login screen itself is browser-redirect only. - Env token: set
MATTERAI_TOKENto skip login entirely (takes precedence over everything). - Dev endpoints:
MATTERAI_BACKEND_URL(defaulthttps://api.matterai.so) andMATTERAI_APP_URL(defaulthttps://app.matterai.so) override where the device flow points — useful against a local backend/webapp. - Tokens are MatterAI JWTs. A token whose payload has
env: "development"automatically routes API calls tohttp://localhost:3000, matching the extension's behavior.
Sign out with /logout (removes the saved token).
The built-in Axon models are listed below; /model opens a scroll-and-select
picker (/model <id> still selects directly). Additional models can be
declared via customModels in settings.json. The choice persists across
sessions.
| id | context | max output | pricing |
|---|---|---|---|
axon-eido-3-code-pro |
400k | 64k | $3/M in · $9/M out |
axon-eido-3-code-mini |
400k | 64k | $1.5/M in · $4.5/M out |
axon-code-2-5-pro |
400k | 64k | $2/M in · $6/M out |
axon-code-2-5-mini |
400k | 64k | free |
axon-eido-3-code-mini is the default. All four support native JSON tool calls
and image input. Cost comes from the API's usage chunks (is_byok-aware) and is
shown in the status bar.
The Axon models go through the MatterAI gateway as before. A customModels
entry that sets a provider is instead served through the
Vercel AI SDK, reusing the same agent loop, tools, and
approvals — auth is the provider's own key (env var or apiKey), not the
MatterAI login.
{
"model": "claude-opus-4-8",
"customModels": [
{
"id": "claude-opus-4-8",
"name": "Claude Opus 4.8",
"provider": "anthropic",
"contextWindow": 1000000,
"maxOutputTokens": 64000,
"inputPrice": 0.000005,
"outputPrice": 0.000025,
"effort": "high"
},
{
"id": "some-model",
"provider": "openai-compatible",
"baseUrl": "https://api.other-host.com/v1"
}
]
}provider: "anthropic"→ native/v1/messages(@ai-sdk/anthropic). Key fromANTHROPIC_API_KEY(orapiKeyon the entry). Adaptive thinking + reasoning streaming are on by default;effort(low…max) tunes depth; prompt caching breakpoints are set on the system prompt and conversation prefix automatically. Thinking blocks are preserved across turns (stored with the session and replayed with their signatures), so interleaved thinking with tool use round-trips correctly. Set"reasoning": falseto disable thinking (e.g. for models that don't supporteffort).provider: "openai-compatible"→ any OpenAI-compatible endpoint; requiresbaseUrl. Key fromapiKeyon the entry.
Note: Third-party models are not shown in the TUI's
/modelpicker. They're still available headlessly viaorbcode -p "..." --model <id>(orMATTERAI_MODEL=<id>); in an interactive session,/model claude-opus-4-8prints the exact command to run.
Anything without a provider (or provider: "matterai") keeps using the
MatterAI gateway untouched.
___ _ ____ _
/ _ \ _ __ | |__ / ___| ___ __| | ___
| | | || '__|| '_ \ | | / _ \ / _` | / _ \
| |_| || | | |_) || |___ | (_) || (_| || __/
\___/ |_| |_.__/ \____| \___/ \__,_| \___|
- Streaming responses rendered as markdown (headers, lists, code fences, inline code, links) via a lightweight ANSI renderer.
- Thinking: reasoning streams live under
✦ Thinking…(last few lines, dimmed) and collapses to✦ Thought for Nswhen done.ctrl+otoggles expanded thinking for subsequent turns. Reasoning arrives from the API asreasoning/reasoning_contentdeltas or inline<think>…</think>blocks — all are routed to the thinking display. - Tool rows: each tool call shows a formatted name ("Read File", "Execute
Command"…), one-line summary (file path, command, query…), live "running"
state, then
✓/✗with a short result preview. - Edit diffs: file-modifying tools render a real diff — stats header ("Added 2 lines, removed 1 line"), line-number gutter, red/green backgrounds — both in the approval prompt (before anything is written) and in the finished tool row.
- Tasks: the model maintains a checklist via
update_todo_list; it renders as a compact Tasks panel (□pending /◧in progress /■done). - @-references: type
@in the input to fuzzy-search workspace files; ↑/↓ to choose, enter/tab inserts the top/selected match into the prompt. - Followup questions:
ask_followup_questionrenders a selectable menu (arrow keys, number quick-pick, or free-text answer). - Completion:
attempt_completionrenders a bordered "✔ Task completed" card with the result. - Status bar: approval mode (
⏵⏵ accept edits on, shift+tab to cycle), busy state, model name, context token usage, and session cost. - Input box: top/bottom rule borders with a
❯prompt, history (↑/↓), cursor movement (←/→, ctrl+a/e), kill line (ctrl+u), multi-char paste (a trailing newline submits), and a slash-command autocomplete menu when the line starts with/. Every menu in the CLI (slash commands, @-files, model picker, session picker, followups) is navigable with ↑/↓ and selectable with enter; a partial command like/mod+ enter runs the highlighted match.
| command | action |
|---|---|
/help |
list commands |
/model |
scrollable model picker (/model pro / /model mini / full id selects directly) |
/clear |
clear the screen only, like the terminal's clear — the conversation and context continue |
/new |
start a fresh conversation/session with a clean slate |
/resume |
pick a previous session for this directory and continue it (screen is cleared, conversation replayed) |
/analytics |
open the MatterAI analytics dashboard (app.matterai.so/orbital) in the browser |
/compact |
summarize the conversation and replace history with the summary |
/tasks |
print the current task list |
/status |
version, model, account, gateway, context usage, cost, approval modes |
/cost |
show session cost and fetch account balance |
/init |
analyze the codebase and create/improve AGENTS.md |
/login |
start the browser sign-in flow |
/logout |
remove the saved token |
/version |
print the CLI version |
/exit |
quit |
| key | action |
|---|---|
Esc |
interrupt the running turn (or cancel login polling / close a menu) |
Ctrl+C |
quit |
Ctrl+O |
toggle thinking display for the whole transcript (past turns included) |
Shift+Tab |
cycle approval mode: ask → accept edits → auto-approve |
↑ / ↓ |
input history, or navigate any open menu |
Ctrl+A / Ctrl+E |
start / end of line |
Ctrl+U |
clear the input line |
Read-only tools (read/list/search/web/todos) run without prompting. Mutating tools prompt first:
- File edits (
file_edit,multi_file_edit,file_write) — prompt shows the target;yallow once,ndeny,aallow for the rest of the session. - Commands (
execute_command) — prompt shows the exact command line. The model classifies commands with anisDangerousflag; dangerous commands (deletes, force-pushes, system changes…) can never be auto-approved — noaoption, and--yolo/session-approval don't apply.
A denial is reported back to the model as "The user denied this operation." so it can adjust course rather than fail.
orbcode -p "explain the build pipeline in this repo"
orbcode -p "fix the lint errors" --yoloPrints only the final content to stdout (the completion result, or the last
assistant message) — no tool activity, no intermediate text. Errors go to
stderr. Without --yolo, edit/command approvals are auto-denied (read-only
analysis). Followup questions are auto-answered with "proceed with best
judgment".
Two kinds of files under ~/.orbcode/:
config.json— state written by the app itself (login token, chosen model, approval defaults). Created on first save, mode 0600.settings.json— user-managed configuration, Claude-Code style. Created automatically as an empty{}on first run so it's easy to find. A project-level.orbcode/settings.jsonin the working directory layers on top of the user-level file.
{
"apiKey": "<token used instead of logging in>",
"baseUrl": "https://my-gateway.example.com/v1",
"model": "my-custom-model",
"autoApproveEdits": false,
"autoApproveSafeCommands": false,
"customModels": [
{
"id": "my-custom-model",
"name": "My Custom Model",
"contextWindow": 128000,
"maxOutputTokens": 32000,
"inputPrice": 0.000001,
"outputPrice": 0.000002
}
],
"env": { "MY_VAR": "value" }
}All keys are optional. customModels entries appear in the /model picker
alongside the built-in Axon models; baseUrl points the chat client at any
OpenAI-compatible gateway; env is applied to the process at startup; hooks
configures lifecycle hooks (see Hooks). Precedence: env vars > project
settings.json > user settings.json > config.json.
Sessions are stored in ~/.orbcode/sessions/<id>.json and power /resume
and --resume <id>.
| env var | effect |
|---|---|
MATTERAI_TOKEN |
auth token (overrides everything) |
MATTERAI_API_KEY |
same as apiKey in settings.json |
MATTERAI_BASE_URL |
same as baseUrl in settings.json |
MATTERAI_MODEL |
model override (what --model sets internally) |
MATTERAI_CONFIG_DIR |
config directory (default ~/.orbcode) |
MATTERAI_BACKEND_URL |
device-auth backend (default https://api.matterai.so) |
MATTERAI_APP_URL |
webapp for the authorize page (default https://app.matterai.so) |
autoApproveEdits / autoApproveSafeCommands set the session defaults for the
approval prompts (dangerous commands still always prompt); shift+tab cycles
them at runtime.
Hooks are shell commands OrbCode runs at fixed points in the agent loop — use
them to block dangerous actions, auto-approve trusted ones, rewrite
tool inputs, inject context into the model, format code after edits,
notify you, or keep the agent working until a condition is met. They use
the same contract as Claude Code's hooks, so scripts written for it work
here (just use $MATTERAI_PROJECT_DIR and OrbCode's tool names).
📖 This is the overview. The complete, example-driven reference — per-event input/output, a copy-paste cookbook, debugging, and security — is in docs/HOOKS.md.
Make OrbCode block rm -rf and append the git branch to every prompt.
1. Drop a guard script at ~/.orbcode/hooks/guard.sh (and chmod +x it):
#!/usr/bin/env bash
input=$(cat) # OrbCode sends JSON on stdin
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
if printf '%s' "$cmd" | grep -Eq 'rm -rf (/|~|\*)'; then
echo "Refusing destructive command: $cmd" >&2 # stderr = the reason
exit 2 # exit 2 = block the tool
fi
exit 02. Register it in ~/.orbcode/settings.json (user-level) or a project's
.orbcode/settings.json — both are merged, so projects can add hooks
without clobbering your global ones. (User hooks always run; project hooks are
disabled until you approve them in a one-time trust prompt, since they run
shell commands from a repo — see Security.)
{
"hooks": {
"PreToolUse": [
{
"matcher": "execute_command",
"hooks": [{ "type": "command", "command": "~/.orbcode/hooks/guard.sh", "timeout": 30 }]
}
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "echo \"Git branch: $(git branch --show-current 2>/dev/null)\"" }] }
]
}
}Start OrbCode normally — that's all. Each event maps to a list of matchers; a
matcher has an optional matcher regex (omit, or use "*", to match
everything; the regex is auto-anchored so "execute_command" matches exactly
that tool name) and a list of command hooks (timeout is per-command
seconds, default 10).
| event | when it fires | matcher tests |
|---|---|---|
SessionStart |
first turn of a session (or after --resume) |
source |
UserPromptSubmit |
before each prompt is sent to the model | — |
PreToolUse |
before a tool runs (and before its approval) | tool name |
PostToolUse |
after a tool returns | tool name |
Notification |
when OrbCode needs permission or a follow-up | — |
Stop |
when the model is about to finish the turn | — |
PreCompact |
before /compact summarizes the conversation |
trigger |
SessionEnd |
on quit, /logout, or end of a -p run |
reason |
SubagentStop |
reserved; OrbCode has no subagents yet | — |
A hook receives a JSON payload on stdin (session_id, transcript_path,
cwd, hook_event_name, plus event fields like tool_name/tool_input,
prompt, …) and influences OrbCode via its exit code — 0 success
(stdout becomes context for UserPromptSubmit/SessionStart), 2 block
(stderr is the reason), other = non-blocking warning — and/or a JSON object
on stdout for fine control (decision, continue, systemMessage, and a
hookSpecificOutput with permissionDecision allow/deny/ask, updatedInput,
additionalContext). When several hooks match, they run in parallel, the most
restrictive permission wins (deny > ask > allow), and a failing/slow hook
is timed out and never crashes the agent. Hooks run with a redacted
environment (your API token and other credential-like vars are stripped) and
injected context is wrapped in <hook_context> tags the model treats as
untrusted.
→ Full reference with worked recipes for every event: docs/HOOKS.md.
src/
index.tsx entry: arg parsing, interactive vs -p (headless) mode
branding.ts product name, logo, colors; VERSION read from package.json
headless.ts non-interactive -p runner
config/settings.ts load/save ~/.orbcode/config.json
auth/auth.ts device flow (start/poll), JWT→backend-URL mapping,
profile/balance fetch, token verification
api/
models.ts the two Axon models (ported from the extension registry)
client.ts OpenAI-compatible streaming client → api2.matterai.so/v1/web
stream.ts chunk model: text / reasoning / native_tool_calls / usage
headers.ts X-AxonCode-Version, X-AxonCode-TaskId, X-AXON-REPO, …
prompts/system.ts system prompt: agent roleDefinition + tool guide (ported
verbatim from the extension) + CLI system-info section
tools/
schemas/ native-tools JSON schemas, copied verbatim from the extension
executors/ CLI implementations (fs, child_process, search, web)
index.ts dispatch, approval classification, call summaries
core/
agent.ts the agent loop (see below)
events.ts AgentEvent model consumed by the UI
hooks.ts lifecycle hooks engine, Claude-Code compatible (see Hooks)
ui/
App.tsx main Ink app: static finalized rows + dynamic streaming area
LoginView.tsx device-flow login screen with paste fallback
components/ Header, InputBox, rows, ApprovalPrompt, FollowupPrompt,
Spinner, StatusBar
markdown.ts markdown → ANSI renderer
Streaming faithfully ports the extension's handler quirks: cumulative
content dedup (some backends re-send full content), <think> blocks routed to
reasoning, both reasoning and reasoning_content delta fields, tool-call
fragments accumulated by index (id/name in the first delta, argument chunks in
the rest), and cost taken from the final usage chunk.
Requests carry the extension-compatible headers (X-Title,
X-AxonCode-Version, User-Agent: orbcode-cli/<version>, per-task
X-AxonCode-TaskId, and X-AXON-REPO set from the git remote or folder name).
Task titles: after the first turn, the backend-generated task title is
fetched once per task from /axoncode/meta/<taskId> (with retries, like the
extension). It shows in the status bar, is written into the session file (so
/resume lists real titles), and becomes the terminal window title:
<title> (orbcode).
Usage data: /status and /cost fetch /axoncode/profile and show the
plan, usage percentage (used/remaining), remaining reviews, and the credits
reset date — the same data as the extension's profile view.
Active in the CLI (schemas byte-identical to the extension's native-tools):
| tool | executor notes |
|---|---|
read_file |
line-numbered output (LINE|content, 6-char pad), 1000-line cap, offset/limit |
file_edit |
single replacement; unique-match enforcement; replace_all; empty old_string = whole file |
multi_file_edit |
batched edits grouped per file, per-edit OK/FAILED results |
file_write |
creates parent dirs, full-content writes |
list_files |
optional recursive, ignores node_modules/.git/build dirs, 800-entry cap |
search_files |
JS regex search with glob file_pattern (picomatch), 300-match cap, binary skip |
execute_command |
user's shell, 120s timeout, 30k output cap, optional cwd |
web_search / web_fetch |
proxied through the MatterAI backend with your token |
update_todo_list |
drives the TUI todo panel |
ask_followup_question |
interactive menu in the TUI |
attempt_completion |
ends the turn with a completion card |
Present in tools/schemas/ but inactive (need IDE services): codebase_search,
lsp, list_code_definition_names, use_skill, check_past_chat_memories,
browser_action, generate_image, new_task, switch_mode,
fetch_instructions, run_slash_command.
Per user message (core/agent.ts):
- The first message is prefixed with
<environment_details>(workspace file listing, git branch/status, time); every user message is wrapped in<user_query>tags, matching the extension's prompt contract. - Stream a completion (system prompt + history + tool schemas, temperature 0.2). Text/reasoning deltas are forwarded to the UI as they arrive; tool-call fragments are accumulated.
- If the model made tool calls: each one is summarized, approval is requested
when required, the executor runs, and the result is appended as a
role: "tool"message.ask_followup_questionblocks on the user's answer;attempt_completionends the turn. - Repeat (max 50 steps per turn) until a plain text response or completion.
Esc aborts the in-flight request via AbortController; the interruption is
recorded in the conversation as a <system_reminder> so the model knows.
npm run dev # run from source (tsx)
npm run build # compile to dist/
npm run typecheck # tsc --noEmitSource-of-truth rule: behavior is ported from the Orbital extension repo
(tool schemas under src/core/prompts/tools/native-tools, prompts in
src/core/prompts/, models in src/api/providers/kilocode-models.ts) — keep
schemas byte-identical rather than editing them here.
Backend/web pieces of the device-auth flow live in:
gravity-console-backend→src/controller/orbcodeAuthController.ts(+ OAuth state inrouter.ts, callbacks inauthController.ts)gravity-console-webapp→ authorize dialog insrc/App.js, sign-in q-p preservation insrc/layouts/authentication/sign-up/index.js
node test-ui.mjs # in-process TUI test with a fake TTY
node test-device-auth.mjs # device-auth polling flow against a local mocktest-ui.mjsdrives the real App (ink-testing-library technique): header, slash menu,/help,/modelswitching, message submission, and a live round-trip to the API gateway — the bundled fake token yields a clean 401 error row. Self-contained: writes its own config fixture to/tmp/orbcode-test-config.test-device-auth.mjsspins up a local HTTP mock of the backend endpoints and verifies: code issuance, pending polls, authorization, one-time token pickup, and expiry semantics.
orbcode: command not found— runnpm linkin this repo; checknpm prefix -g's bin dir is on your PATH.--versionshows an old version — rebuild (npm run build); the linked command runsdist/, and the version is read frompackage.jsonat runtime.- Stale link after the rename —
npm unlink -g orbitalcode && npm link. - Login times out — the device code lives 10 minutes; press Enter to retry,
or paste a token manually. Against a local stack, set
MATTERAI_BACKEND_URLandMATTERAI_APP_URL. - 401 on chat — token expired:
/logoutthen/login. - Keyboard input does nothing — OrbCode needs a real TTY (raw mode); it
won't accept piped stdin. Use
-pfor non-interactive runs. EPERM: operation not permittedopeningbin/orbcode.json macOS — the repo lives in a protected folder (Documents, Desktop, Downloads) and the terminal app hasn't been granted access to it. Allow it in System Settings → Privacy & Security → Files and Folders (or Full Disk Access) for that terminal, then restart it. Terminals that already prompted for access (e.g. iTerm) keep working. A normal global install (npm install -g @matterailab/orbcode) is unaffected because it lives outside protected folders.
The README covers the essentials. For the complete, kept-up-to-date reference (guides, configuration, hooks cookbook, troubleshooting), visit the docs site:
👉 https://docs.matterai.so/orbcode-cli/overview
Contributions are welcome! See CONTRIBUTING.md for how to set up a development environment, run the tests, and submit a pull request. Bug reports and feature requests go to GitHub Issues.
MIT © MatterAI
