Welcome to Octoscode — Your Coding Buddy██████╗ ██████╗████████╗ ██████╗ ███████╗ ██╔═══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔════╝ ██║ ██║██║ ██║ ██║ ██║███████╗ ██║ ██║██║ ██║ ██║ ██║╚════██║ ╚██████╔╝╚██████╗ ██║ ╚██████╔╝███████║ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝
octoscode is the terminal app for Octos
— an AI coding assistant in your terminal, in the spirit of Claude Code and
Codex. The Octos server runs the agent, the models, and the tools; octoscode
is the fast, keyboard-driven way to talk to it: chat, diffs, tool approvals,
background tasks — all without leaving the shell.
Install just the TUI — it auto-provisions the Octos server (the brain) on first launch, so there's nothing else to set up:
npm install -g @octos-org/octoscode
# or Homebrew (this repo is its own tap):
# brew tap octos-org/octoscode https://github.com/octos-org/octoscode
# brew install octos-org/octoscode/octoscode
# (or the shell / PowerShell installer — see Install below)Then just run it:
octoscodeOn first launch the TUI downloads the matching Octos server into ~/.octos/bin
(binary-only — no background service) and spawns it over stdio, then drops
you on the "Welcome to Octos" screen. In the next five minutes: create your
local profile (three fields — the email is local metadata only), pick an AI
provider, paste its API key, and open your first coding chat. The
Quickstart below walks every screen.
Just looking?
octoscode --mode mockopens a mock demo with canned replies — no server, connected to nothing. Plainoctoscodeis the real thing.
Want it in a browser too? The solo launch above talks to its server over stdio, which serves exactly one client. Run the server on a port instead and the terminal and octoscode-web can both attach — to the same sessions, at the same time. See Two ways to run.
| Symptom | Fix |
|---|---|
| First launch can't fetch the server | Auto-install needs network. Offline / behind a proxy? Install octos yourself (npm i -g @octos-org/octos, or the server guide) — the TUI then finds it. Set OCTOSCODE_NO_AUTO_INSTALL=1 to disable auto-install. |
| Replies are instant and feel canned | You launched with --mode mock. Run plain octoscode for the real backend. |
| "Test provider" fails during onboarding | Re-check the API key and the provider choice; you can redo it anytime with /onboard or /setup. |
More in the full Troubleshooting table below.
On a fresh first launch the main window shows the OCTOS block-letter wordmark with the tagline "Welcome to Octos — Your Coding Buddy" above a short onboarding menu — your starting point for the walkthrough below.
octoscode is intentionally separate from octos-cli: the octos repo owns
the server/runtime and the shared octos-core protocol types; this repo owns
the terminal client. Architecture and ownership boundaries live in
docs/ARCHITECTURE.md.
Every method installs a single self-contained octoscode binary. Then run
octoscode --help.
Same model as Claude Code and Codex: each GitHub Release ships prebuilt binaries for macOS (Apple Silicon), Linux (x86-64 + arm64), and Windows (x86-64). Pick one — each block has its own copy button (top-right corner, on hover) that copies just that command:
📦 npm
npm install -g @octos-org/octoscode🍺 Homebrew — this repo is its own tap
brew tap octos-org/octoscode https://github.com/octos-org/octoscode
brew install octos-org/octoscode/octoscode🐚 Shell installer — macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/octos-org/octoscode/releases/latest/download/octoscode-installer.sh | sh🪟 PowerShell installer — Windows
powershell -ExecutionPolicy Bypass -c "irm https://github.com/octos-org/octoscode/releases/latest/download/octoscode-installer.ps1 | iex"Once installed, octoscode update checks for a newer release — and for
shell/PowerShell-installer installs it self-updates in place; npm/brew/cargo
installs are owned by their package manager, so it prints the matching
upgrade command instead. octoscode doctor diagnoses the local environment
and connection prerequisites.
From git — no crates.io publish required
cargo install --git https://github.com/octos-org/octoscode octoscodeFrom crates.io — once published
cargo install octoscode
octos-core(the shared protocol crate) is pulled automatically as a git dependency, so installing needs no siblingoctoscheckout.
A copy-pasteable, first-time walkthrough. By the end you have a local profile, an LLM provider, and a live coding session — no dashboard, no email OTP.
"Solo" here means one agent. For the two-agent setup — a cheap model working under review by a strong one, with push authority held by the reviewer — see Dual loop: OctoLoop.
Install octoscode as shown in Start here — that's all you need.
On first launch it downloads the matching Octos server into ~/.octos/bin
automatically (binary-only, no service), so there's no separate server install.
(Already have octos on your PATH? The TUI uses it, as long as it's a
compatible version.)
Building from source works too — octos-core (the shared protocol crate) is
pulled automatically as a git dependency, so a plain clone builds with no
sibling checkout required (needs Rust 1.85+):
git clone https://github.com/octos-org/octoscode.git
cd octoscode
cargo build --release
# produces ./target/release/octoscode — substitute it for `octoscode` belowDeveloping against a local
octos? To build against an uncommitted sibling../octos/crates/octos-coreinstead of the pinned git revision, runcp .cargo/config.toml.example .cargo/config.toml(gitignored) — see the comment in that file. This restores the live-sibling edit loop of the old path dependency.
Just run it — the TUI provisions and launches the server for you:
octoscodeYou land on the "Welcome to Octos" screen (subtitle "Set up a local solo profile to continue."), with the OCTOS wordmark above the menu.
Notes:
- On first launch the TUI downloads the matching
octosserver into~/.octos/binand spawns it over stdio as a child — one command, no separate install, no background service. - A fresh setup (no prior profile in
~/.octos) lands on the welcome screen; if you already have a profile there, it opens straight into a session. - Advanced — point at your own server instead:
--stdio-command "octos serve --stdio --solo --data-dir <dir>"for a custom local backend, or--endpoint ws://host:port/api/ui-protocol/wsfor a remote one. Do not pass--profile-idon a true first run — it selects an existing profile and skips onboarding.
On the welcome screen, fill the three fields (the email is local metadata only — no OTP is sent):
| Field | How to enter it |
|---|---|
| Full name | select the row and type, or /onboard name <your name> |
| Username | select the row and type, or /onboard username <handle> |
select the row and type, or /onboard email <address> |
Then choose "Create your local Octos profile" / Continue. This calls
profile/local/create and advances to provider setup.
The screen now reads "Set Up LLM Provider" ("Choose a dashboard model route, enter its API key, then save."). Work down the rows:
- Load provider catalog — pulls the dashboard's model families and routes.
- Model family → Model → Provider route — pick one route.
- API key — select the row and type the key (
/onboard key <secret>); it is masked in state, logs, and snapshots. - (optional) Test provider to verify the route.
- Save provider to profile — persists it via
profile/llm/upsert(the same profile JSON the dashboard writes).
The catalog and provider schema are owned by octos/the dashboard; the TUI
never hard-codes provider/model truth.
Once a provider is saved, choose "Open coding session". This calls
session/open with the resolved profile and drops you into the normal coding
UI. Type a request in the composer and press Enter — you're chatting with Octos.
You can reopen this wizard at any time with the /setup slash command.
A coding session drives an agent that reads and (optionally) edits code in your
workspace. How much it may do is a per-session setting you change live with
/permissions — no restart, no launch flag:
| Mode | The agent can… | Use it for |
|---|---|---|
| Read-only | read files, run read-only commands (git diff, grep); writes fail |
code review — it can't change your repo |
| Workspace-write | read + write inside the workspace | hands-on edits, scoped to your project |
| Full Access ("yolo") | host filesystem + network, approvals never | trusted local automation — risk of data loss |
So for a review, run /permissions → Read-only and ask the agent to review
the diff; for hands-on changes, switch to Workspace-write (or Full
Access). The TUI only requests the mode — the backend applies it, and Full
Access is offered only on solo/local backends, never on a shared octos serve.
For headless / scripted code review and for running many review or edit
agents in parallel, use the octos chat CLI in the main
octos repo (--sandbox, --yolo,
--profile, --no-session-persistence) — see its README's Headless agent mode
& code review section.
Everything above runs one agent. OctoLoop runs two, with different price tags and different authority: a cheap model does the work on the inside, and a strong model reviews it from the outside. The protocol underneath is OLP (Outer-Loop Protocol); OctoLoop is the packaged, one-command form of it.
┌─ OUTER loop (strong model: Claude Code / Codex / any CLI agent)
│ read board → dispatch a numbered entry → independently re-verify → push
│ ▲ │
│ .octos/OUTER_LOOP_REVIEW.md (the blackboard) │ herdr prompt / octos steer
│ │ ▼
└─ INNER loop (octoscode + octos serve, running a cheap model such as kimi)
read board → execute → commit (never push) → ACK(done|wontdo|blocked)
The two loops talk only through a file: an append-only blackboard at
.octos/OUTER_LOOP_REVIEW.md. The inner loop takes the lowest-numbered entry
that has no ACK( line, does it, commits, and writes back one of
ACK(done|wontdo|blocked): <notes>. Push authority belongs to the outer loop
alone — so nothing reaches your remote until a second, stronger model has
re-run the tests itself, in an isolated worktree, from the CI workflow verbatim.
That split is the point: you spend cheap tokens on the work, which you can re-run freely, and expensive tokens only on review and adjudication.
cd your-project/
curl -fsSL https://raw.githubusercontent.com/octos-org/octoscode/main/scripts/olp-init.sh | bashIt is idempotent and never overwrites an existing file. It lays down:
| Artifact | What it is |
|---|---|
.octos/OUTER_LOOP_REVIEW.md |
the blackboard, gitignored — it is branch-independent, and tracking it causes cross-branch split brain |
.octos/loop.md |
the inner loop's maintenance cycle |
AGENTS.md |
the self-contained onboarding card (see below); OLP_INIT_LANG=zh for Chinese |
~/.octos/outer/ |
the board sentry and the atomic append helper |
Then start the inner loop:
octoscode --stdio-command 'octos serve --stdio --solo --danger-full-access'--solo is the safety gate for a single-person local box; without it serve
refuses the permissive profile. --danger-full-access matters more than it
looks: the lower permission tiers run the agent inside a filesystem sandbox
where ~/.cargo and ~/.rustup are invisible, so every build command comes
back "command not found" — this is the real cause behind an inner loop
reporting "there is no cargo on this machine". Granting it is an operator
decision; see Agent permissions.
AGENTS.md is the card, and it is deliberately self-contained — any agent
(Claude Code, Codex, whatever you have a subscription for) can read that one
file and take a role without opening the rest of the docs. It covers role
selection, the ACK grammar, dispatch, waking the inner loop, three-layer
observation, isolated re-verification, and the red lines. It is also the
protocol's own resident channel: octos injects AGENTS.md into every session.
The outer loop is not a chat window — it drives the inner one programmatically:
herdr agent list # find the inner pane
herdr agent prompt <pane> '<one line>' # wake it when idle (arrives as a user message)
cd <project> # steer resolves the instance by cwd
octos steer --session '<key>' --text '[external-reviewer] ...' # mid-turn, no interruptherdr is a terminal workspace manager
and is recommended, not required — without it, fall back to tmux send-keys.
For observation, watch all three layers (screen, events.jsonl, and
octos goal status / ledger tail): delivery, consumption and execution are
three different things, and a goal that tripped its breaker is silent in
exactly the same way as one that is still working.
| Doc | For |
|---|---|
docs/OCTOLOOP_AGENTS.md |
the self-contained card — hand this to an agent (中文) |
docs/OLP_QUICKSTART.en.md |
zero-to-running for a new project (中文) |
docs/OUTER_LOOP_PROTOCOL.md |
the protocol in full: R1–R7, schemas, budgets, field lessons |
docs/OLP_OUTER_BOOT.md |
the outer operator card and tactics handbook |
docs/OCTOLOOP_GUIDE.md |
full guide, mechanisms, platform matrix |
docs/OCTOLOOP_FEATURES.md |
one-page capability panorama |
On platforms: Linux is full power. macOS works with two gaps — the outer-duty authority lock is Linux-only, and the sandbox tiers do not apply. On Windows, use WSL2.
The TUI never contains the agent — octos serve does. What changes is how the
two talk, and that decides whether anything else can join.
| Solo (stdio) | Server (WebSocket) | |
|---|---|---|
| Start it with | octoscode |
octos serve --host … --port …, then octoscode --endpoint … |
| Who runs the server | the TUI spawns and auto-provisions it | you do, and it outlives the client |
| How many clients | exactly one | as many as you point at it |
| Browser client | no | yes — octoscode-web |
| Setup | none | a port and a shared token |
Solo is the default because it is the shortest path to a working session.
--stdio runs the protocol over the child's stdin and stdout instead of
binding HTTP, so there is no port for anything else to reach — a browser cannot
attach to a solo launch, however it is configured.
If a server is already running (locally or remote), connect over its UI Protocol WebSocket instead of spawning a child:
# terminal 1 — the server, bound to a port
# (~/.octos/bin is where octoscode installs it; it is not on your PATH)
export OCTOS_AUTH_TOKEN=local-dev-token
~/.octos/bin/octos serve --host 127.0.0.1 --port 50080 --auth-token "$OCTOS_AUTH_TOKEN"
# terminal 2 — the TUI, attaching to it
octoscode --endpoint ws://127.0.0.1:50080/api/ui-protocol/wsWith --endpoint the TUI provisions nothing: it does not download a server
binary and does not spawn a child, because there is no stdio command to run
(backend_ensure.rs returns early — "WebSocket launch
— no local backend to provision"). The server you started is the only one.
Or from a source checkout of the sibling repo:
cd ../octos
export OCTOS_AUTH_TOKEN=local-dev-token
cargo run -p octos-cli --features api --bin octos -- serve \
--host 127.0.0.1 --port 50080 \
--cwd "$PWD" \
--data-dir /tmp/octoscode-dev-data \
--auth-token "$OCTOS_AUTH_TOKEN"Then connect in another terminal:
octoscode \
--mode protocol \
--endpoint ws://127.0.0.1:50080/api/ui-protocol/ws \
--auth-token local-dev-token \
--cwd "$PWD/my-project"Use the same token for --auth-token on both sides (or set
OCTOS_AUTH_TOKEN). Add --profile-id <id> to open an existing profile and
skip onboarding; add --readonly for a view-only session that never sends
turns.
Three terminals, one server. Pick a token — any string you like — and use the same one everywhere below.
Before you start. Run octoscode once on its own (see
Start here). That downloads the server to ~/.octos/bin/octos.
It is not added to your PATH, so the commands below spell out the full
path. You also need Node.js 22+ and pnpm for the browser
client; corepack enable installs the right pnpm.
1. Start the server — terminal 1:
export OCTOS_AUTH_TOKEN=my-local-token
~/.octos/bin/octos serve --host 127.0.0.1 --port 50080 \
--auth-token "$OCTOS_AUTH_TOKEN" --solo--solo is what lets the browser set up your profile on first run. Leave it
out and a fresh server answers "This server cannot onboard from the Web".
2. Start the browser client — terminal 2:
git clone https://github.com/octos-org/octoscode-web.git
cd octoscode-web
pnpm install --frozen-lockfile
OCTOSCODE_DEV_PROXY_TARGET=http://127.0.0.1:50080 \
OCTOSCODE_DEV_PROXY_ORIGIN=http://127.0.0.1:50080 \
pnpm devKeep both OCTOSCODE_DEV_PROXY_* variables. They make the browser talk only to
the web client's own address, which forwards to the server. Leave them out and
the server refuses the browser, because by default it trusts only its own
address — and the page says "Could not connect", blaming your server and token
even though both are fine.
3. Open the browser at the address step 2 prints — usually http://127.0.0.1:4173, or the next free port if that one is taken.
- Leave Server origin as it is — it already points at this page.
- Paste your token into Auth token and select Connect.
- Under Add workspace, type the full path of a project folder and select Start session.
- The first time only, Create your local coding profile opens. Set
Profile ID to
main(step 4 uses it), choose a Provider and Model, and paste the provider's API key.
4. Attach the terminal — terminal 3, using the Profile ID from step 3:
OCTOS_AUTH_TOKEN=my-local-token octoscode \
--endpoint ws://127.0.0.1:50080/api/ui-protocol/ws --profile-id mainThe status line reads "Pass --session to open an interactive session." That is
expected, not an error: type /resume to pick the conversation you started in
the browser.
If a step fails
| You see | Fix |
|---|---|
command not found: octos |
Use the full path, ~/.octos/bin/octos — see Before you start |
| "This server cannot onboard from the Web" | Restart the server with --solo (step 1) |
| "Could not connect" in the browser, though the server is running and the token is right | Restart step 2 with both OCTOSCODE_DEV_PROXY_* variables set |
| The TUI will not start: "endpoint and stdio-command cannot both be configured" | Your ~/.config/octoscode/config.json has a stdio_command; see Config file |
Nothing happens after /resume |
The browser session has not finished a turn yet — send one message there first |
Hosting the browser client somewhere else? If you serve it without that
proxy, the server has to be told to trust its address: set
OCTOS_APPUI_ALLOWED_ORIGINS=http://<client host>:<port> on the server, or
appui.allowed_origins in its config. It trusts only its own address otherwise.
Octos
v2.0.3-rc.12can print a one-time pairing link withoctos serve --web-url …; the browser can open it without copying a token.
Both clients can open the same session. Pass --session <id> here and pick
that session in the browser, and you get one conversation with two front ends:
session/open attaches rather than claims, the server replays what each client
missed from its own cursor, and every later event for that session is pushed to
every attached connection. A turn started in the terminal streams into the
browser while it runs, and the reverse.
Two consequences worth knowing before you rely on it:
- One turn at a time per session. The server keeps a single active-turn slot
per session; while one client's turn runs, the other's
turn/startis refused rather than queued server-side. - Neither client owns the session. Either may start a turn whenever the slot
is free, and any attached client can interrupt the running turn — the abort
path keys on the session and turn, not on the connection that started it. Use
--readonlyon the TUI when you want it to watch without sending.
Attaching is not just a different address. Four things change, and all four have produced confusing sessions:
- No pairing link. The TUI is token-only.
octos serve --web-urlprints a link for the browser; the TUI has no/pair/claimsupport, so give it--auth-tokenorOCTOS_AUTH_TOKEN. The token rides anAuthorization: Bearerheader on the WebSocket upgrade, not a query parameter. - No local profile discovery. Attached launches skip the profiles data dir,
because a remote server's profile registry is not visible from here. The
profile picker and
/profilesare unavailable; pass--profile-id <id>. --sessionis optional, and omitting it opens nothing. You connect and the status line says so — "Pass --session to open an interactive session." Pick one with/resume, or pass the id up front.- A bare session id can hit
profile '_main' is not configured. Some status reads carry only the session id, and on the WebSocket transport the server resolves the profile from the authenticated identity — an admin token has none — falling back to_mainregardless of--profile-id. Sessions the TUI creates carry their profile in the key and are fine; a hand-passed id that does not is not. Use a profile-prefixed session id, or authenticate as that profile's user rather than with an admin token.
octoscode doctor probes a configured endpoint with config/capabilities/list
and is the fastest way to tell a bad address from a bad token.
For render/keyboard/theme smoke tests with no backend at all:
cargo run -- --mode mock
cargo run -- --mode mock --theme claude--mode mock is an explicit opt-in. A bare launch (no --mode/--endpoint/
--stdio-command) defaults to protocol and auto-provisions a local server —
so plain octoscode is the real thing, not the mock.
--config <json-file> JSON launch config; CLI flags override its values
--mode mock|protocol mock (no server) or protocol (live). Default: protocol
(a bare launch auto-provisions a local server)
--endpoint <ws-url> UI Protocol WebSocket (ws:// or wss://)
--stdio-command "<cmd>" spawn an `octos serve --stdio` child instead of --endpoint
--session <session-id> session to open first
--profile-id <id> existing profile to use (skips onboarding)
--cwd <dir> workspace cwd to request; defaults to the launch dir
--auth-token <token> bearer token; falls back to OCTOS_AUTH_TOKEN
--readonly / --no-readonly open as a view-only session, or force read-write
--theme <name> codex | claude | slate | solarized | terminal
--lang en|zh UI language; falls back to OCTOS_LANG / LANG. Default: en
--scroll-mode <mode> native (terminal scrollback, default) | pinned (composer pinned)
--vim-mode enable Vim modal editing in the composer (default off)
--steer-mid-turn inject a prompt typed mid-turn into the RUNNING turn
(default off: mid-turn prompts queue FIFO and each runs
as its own turn, in the order typed)
--no-splash skip the startup logo animation
--endpoint and --stdio-command are mutually exclusive — pick one transport.
Do not put provider or model anywhere: those are server-owned Octos
settings loaded by octos serve, and the TUI config rejects them.
~/.config/octoscode/config.json is read on every launch, with no flag —
--config FILE points at a different one. CLI flags win on conflict. Print the
path with octoscode config path.
{
"mode": "protocol",
"stdio_command": "octos serve --stdio --solo --data-dir ./octos-data",
"session": "coding:local:main",
"profile_id": "coding",
"cwd": "/path/to/project",
"readonly": false,
"theme": "codex",
"lang": "en",
"scroll-mode": "native",
"vim-mode": false,
"steer-mid-turn": false
}/saveconfig writes the active theme / lang / scroll-mode / vim-mode / steer-mid-turn
back into this file (merging — it never clobbers transport keys like
stdio_command); without --config it falls back to
~/.config/octoscode/config.json.
To attach to a running server instead, swap the transport key (the file is strict JSON — unknown keys are rejected, and there are no comments):
{
"mode": "protocol",
"endpoint": "ws://127.0.0.1:50080/api/ui-protocol/ws",
"auth-token": "local-dev-token",
"cwd": "/path/to/project"
}endpoint and stdio_command are one choice, here as on the command line. A
config file carrying stdio_command makes octoscode --endpoint … fail to
start — "endpoint and stdio-command cannot both be configured" — because the
flag and the file each supply one half. Remove the key, or point --config at
a file that omits it.
codex, claude, slate, solarized, terminal
terminal keeps foreground/background on your terminal defaults where ratatui
allows it, using only restrained ANSI colors for borders, accents, and errors.
Set the palette at launch with --theme <name>, or switch live with /theme
(a *-marked menu; the change repaints immediately and survives reconnects).
Every interactive launch opens with a short ttfx-rendered OCTOS logo animation on the main screen, picked at random from a curated set:
beams, sweep, wipe, rain, slide, scattered, middleout, highlight, matrix
Each effect runs to its natural end (~2–4s), settles on the full logo for a
beat, then the TUI starts. Press any key to skip straight in. The animation
never blocks startup: it is skipped automatically when stdout is not a TTY,
when CI is set, or when the terminal is smaller than the logo, and any
internal error silently falls through to a normal launch.
--no-splashorOCTOSCODE_NO_SPLASH=1turns it off.OCTOSCODE_SPLASH_EFFECT=matrixpins a specific effect (any name from the curated set; unknown names fall back to the random pick).
Tab peek a running sub-agent's output; Tab/Shift+Tab cycle main↔agents, Esc returns to chat
PgUp/PgDn scroll the transcript (PgUp also opens the pager)
y / s / n approve once / approve for session / deny a pending tool approval
Alt+A re-show the pending approval prompt
[ / ] select previous / next inline diff hunk
c stage the selected hunk as next-turn context
Ctrl+U clear the composer
Ctrl+C interrupt the active turn; with nothing to interrupt, press twice to quit
Ctrl+Q quit immediately, from any surface (incl. wizard/menus)
Esc with no active turn: cancel the first running background task
q quit
/help local slash-command help
/ps show local task/process status and focus the Tasks pane (Esc returns to the composer)
/stop interrupt the active turn (or report locally if none is active)
/setup reopen the onboarding wizard
/model browse the server-returned profile models / catalog
/permissions set the session's sandbox + approval mode (menu): Read-only,
Workspace-write, or Full Access (the "yolo" mode — host access,
network, approvals never). Solo/local backends only.
/theme switch the TUI palette at runtime (menu, or /theme claude)
/lang switch the UI language (menu, or /lang zh) — English / 中文
/thinking set reasoning effort for thinking models, per session (menu, or /thinking high)
/scrollmode switch wheel-scroll behavior (toggle, or /scrollmode native|pinned)
/vimmode toggle Vim modal editing in the composer (Normal/Insert)
/saveconfig persist the active theme / language / scroll-mode / vim-mode / steer-mode to the config file
/steer switch what Enter means mid-turn: on injects into the running turn, off (default) queues FIFO
/onboard set onboarding fields inline (name, username, email, key, ...)
/copy copy the last assistant reply to the clipboard (works over SSH)
/status snapshot-backed session, runtime, and connection status
/cost server-reported token and cost usage
/title configure terminal-title items
/keymap inspect and edit TUI key bindings
/login sign in with email OTP, or inspect current auth state
/exit quit the TUI
Sessions and autonomy (shown when the server advertises the capability):
/resume switch to a prior session and reload its transcript (alias: /sessions)
/rewind go back to an earlier checkpoint in this session to edit & resend (alias: /backtrack)
/loop create, list, pause, resume, fire-now, or delete backend loops
/goal view, set, pause, resume, or clear the persisted session goal
/resume lists sessions newest-first; /rewind shows codex-style checkpoint
rows (#n message preview) and rolls the session back to the one you pick, so
you can edit and resend from there. When a session has loops, the status bar
shows a loop chip (active/paused), and the context gauge reflects the real
per-model context window reported by the server, not a fixed default.
/activity (search sessions/tasks/activity) and /statusline (status-bar
items) are always available. Further capability-gated commands
(/provider, /permissions, /mcp, /tools, /skills, /task,
/threads, /turn, /agents, /review) appear in the / popup only when
the connected server supports them — /help always lists what is live.
/model, /theme, /lang, and /thinking open a selection menu when run with
no argument (or apply inline with an arg). In every selection menu the active
choice is marked with a leading * (distinct from the > navigation cursor).
Slash-command completion is two-step, like Codex: pick an entry from the /
popup (or type a prefix and press Enter) and the full /command lands in the
composer; press Enter again to run it (or type an argument first). Typing a
command's exact name and pressing Enter runs it directly. This is uniform for
every command.
Unknown slash commands are handled locally with a warning and are not sent to the model.
The composer is multi-line: Enter sends, Shift+Enter (or Ctrl+J as a portable fallback) inserts a newline, and the box grows as you add lines.
Arrow Up from an empty composer recalls your command history — newest first, persisted across sessions, shell-style; once browsing, Down steps back toward newer entries. With text present the arrows move the cursor between lines (and fall back to scrolling the transcript at the first/last line). Emacs-style keys also work (Ctrl+A/E, Alt+B/F, Ctrl+W, Ctrl+K, …).
Vim mode is opt-in — --vim-mode, config "vim-mode": true, or /vimmode
at runtime; the composer title then shows NORMAL / INSERT. It implements a
pragmatic subset:
motions h l j k 0 $ w b e gg G
edits x dd dw cc
insert i a A I o O (Esc returns to Normal)
Enter still sends in both modes. Visual mode, registers/yank-paste, and numeric
counts (3dd) are out of scope.
By default (native scroll-mode) the wheel scrolls the terminal's own
scrollback, so native selection/copy stay intact and the composer scrolls away
with the screen. Press Ctrl+T (or PageUp) to open a full-screen
transcript pager where history scrolls in the upper pane while the composer
stays pinned to the bottom; Esc (or Ctrl+T again) closes it.
--scroll-mode pinned (or /scrollmode pinned) opts into app-side wheel
handling: the wheel always scrolls the transcript and the composer never moves,
at the cost of native mouse selection (use Shift+drag). Settled tool-activity
groups collapse to a one-line summary; Ctrl+O expands them — the same
toggle also expands the diff preview's selected hunk in full.
Assistant replies render markdown live as they stream: headings, lists,
checkboxes, blockquotes, tables, fenced code blocks with syntax highlighting
(theme-matched, following /theme), inline bold/italic/code, ~~strikethrough~~,
--- rules, and [links](url). Link urls render in full so the terminal can
make them cmd/ctrl+clickable in the native scroll flow.
While a thinking model reasons, the transcript shows a terse codex-style
· thinking… indicator instead of the verbose reasoning stream; the reply
replaces it when the answer starts. Control the effort with /thinking.
The UI is fully localized in English and Simplified Chinese (中文) — menus,
the command palette, the onboarding wizard, transcript/status surfaces. Pick the
language at launch with --lang {en,zh} (or OCTOS_LANG / LANG), or switch at
runtime with /lang (a *-marked menu) — no restart needed. English is the
source/fallback locale, so any untranslated string falls back to English.
| Variable | Purpose |
|---|---|
OCTOS_AUTH_TOKEN |
Fallback bearer token for the UI Protocol WebSocket. |
OCTOS_LANG / LANG |
UI language fallback when --lang is unset. |
RUST_LOG=off |
Keeps terminal output clean for live visual runs. |
TERM=xterm-256color |
Avoids missing terminfo/color issues on remote hosts. |
OCTOSCODE_BIN |
Forces a specific built octoscode binary for harnesses. |
OCTOSCODE_DIR |
Points Octos harness scripts at this standalone TUI repo. |
OCTOSCODE_NO_AUTO_INSTALL |
Disables backend auto-install (a missing octos then errors). |
OCTOSCODE_NO_SPLASH |
Disables the startup logo animation (same as --no-splash). |
OCTOSCODE_SPLASH_EFFECT |
Pins the splash to one curated effect, e.g. matrix. |
Renamed from
octos-tui. EveryOCTOS_TUI_*variable is nowOCTOSCODE_*. The one exception that still works isOCTOS_TUI_NO_AUTO_INSTALL— it is honoured with a one-time deprecation notice so an existing CI job or shell profile does not silently get auto-install switched back on. Rename it; the fallback goes away a release or two after the rename settles.
octoscode requests a session cwd through session/open. By default that is the
terminal launch directory; --cwd DIR overrides it. octos serve
canonicalizes the requested path and accepts it only if it is inside the
server-approved roots — so start the server with a --cwd that contains the
project you want to work in. For a remote server, pass a --cwd that exists on
the server host. An out-of-bounds cwd fails session/open with a typed
protocol error instead of silently running tools elsewhere.
The AppUi backend agent is created when octos serve starts. If you add or
change the provider/model after the server is already up (via the dashboard or a
hand-edited profile), restart octos serve before opening a new coding session.
Hooks run a command at agent lifecycle events. They are server-side profile
config — edit them on the host that runs octos serve; the TUI just shows their
effects. The command is an argv array (no shell interpretation, so no pipes
or globs), the environment is sanitized, and a leading ~ in command[0] is
expanded. A hook can observe an event, deny it, inject context, or rewrite a
pending tool call.
Hooks (and env_vars, sandbox, plugins, memory, and the skills layer
below) can be declared in two places:
- Per-profile — under
config.hooksin~/.octos/profiles/<id>.json. Fires only for that profile. - Globally — as a top-level
hooksarray in<registry-root>/profile-defaults.json(typically~/.octos/profile-defaults.json). Every profile inherits it.
The two stack: the global hooks run first (in file order), then the
profile's own — both fire. This defaults-under-profile inheritance is the same
mechanism used for env_vars, sandbox, plugins, memory, and the skill
layering described in Custom skills.
{
"hooks": [
{
"event": "after_tool_call",
"command": ["ruff", "check", "--quiet"],
"timeout_ms": 8000,
"tool_filter": ["write_file", "edit_file"],
"path_filter": ["**/*.py"],
"requires_bin": "ruff"
}
]
}| Field | Meaning |
|---|---|
event |
Which lifecycle event triggers the hook (table below). Required. |
command |
Argv array — command[0] is the program, the rest are arguments. |
timeout_ms |
Kill the hook after this many ms (default 5000). |
tool_filter |
Tool events only: fire only for these tool names. Empty = all tools. |
path_filter |
Tool events only: fire only when the tool's args.path matches one of these glob patterns. Tools with no path argument are skipped. |
requires_bin |
Skip the hook unless this binary is on PATH (ship optional linters without forcing every host to install them). |
| Event | When it fires | Can deny? |
|---|---|---|
user_prompt_submit |
Once, when a real user prompt enters a turn, before the first LLM call. | Yes |
before_tool_call |
Before each tool executes. | Yes |
after_tool_call |
After each tool returns. | No |
before_llm_call |
Before each LLM iteration within a turn. | Yes |
after_llm_call |
After each LLM response (carries token / cost / provider stats). | No |
on_turn_end |
When a turn settles. | No |
on_resume |
When a session resumes. | No |
before_spawn_verify / on_spawn_verify / on_spawn_complete / on_spawn_failure |
Background sub-agent (spawn) lifecycle. | before_spawn_verify only |
user_prompt_submit is distinct from before_llm_call: it fires once per
user turn, while before_llm_call fires on every LLM iteration inside that
turn.
The hook receives a JSON payload on stdin and signals its verdict via exit
code. The payload always carries event, plus session_id, profile_id, and
model / cwd where relevant:
- Tool events add
tool_nameandarguments, and (after)result,success,duration_ms. Arguments/results forshell,read_file, andwrite_fileare redacted; other tools are truncated to 1 KB. user_prompt_submitadds theprompttext and the turn'scwd.
| Exit code | Meaning |
|---|---|
0 |
Allow. For user_prompt_submit, anything printed to stdout is injected as extra per-turn context for the model. |
1 |
Deny — for the before-events above only (blocks the operation; the stdout message is surfaced). On after-events, exit 1 is treated as an error. |
2 |
For before_tool_call / before_spawn_verify: replace the pending arguments with the JSON printed on stdout. |
| other | Error (logged, does not block). |
A hook that fails (unexpected non-zero, timeout, or spawn error) 3 consecutive times is disabled by a circuit breaker until the server restarts.
Inject live git state into every turn — user_prompt_submit, exit 0, stdout
becomes per-turn model context:
#!/usr/bin/env bash
# ~/.octos/hooks/git-context.sh
set -euo pipefail
payload="$(cat)"
cwd="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("cwd") or ".")')"
cd "$cwd" 2>/dev/null || exit 0
echo "git branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '(not a git repo)')"
git status --short 2>/dev/null | head -20
exit 0 # allow the turn; stdout is added to the model's context{ "event": "user_prompt_submit", "command": ["~/.octos/hooks/git-context.sh"], "timeout_ms": 4000 }Deny a prompt that leaks a secret — user_prompt_submit, exit 1, the turn never
reaches the LLM:
#!/usr/bin/env bash
# ~/.octos/hooks/no-secrets.sh
set -euo pipefail
prompt="$(cat | python3 -c 'import json,sys; print(json.load(sys.stdin).get("prompt",""))')"
if printf '%s' "$prompt" | grep -Eq 'AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----'; then
echo "Blocked: the prompt appears to contain a credential."
exit 1
fi
exit 0{ "event": "user_prompt_submit", "command": ["~/.octos/hooks/no-secrets.sh"], "timeout_ms": 3000 }Lint Rust files after they are written — after_tool_call scoped by
tool_filter + path_filter, gated on cargo being installed:
{
"event": "after_tool_call",
"command": ["cargo", "clippy", "--quiet"],
"timeout_ms": 20000,
"tool_filter": ["write_file", "edit_file"],
"path_filter": ["**/*.rs"],
"requires_bin": "cargo"
}Stacking global + per-profile: put the secret-guard in the global defaults so it protects every profile, and add the Rust linter to just your coding profile.
// ~/.octos/profiles/coding.json — "config.hooks", only this profile
{ "config": { "hooks": [
{ "event": "after_tool_call", "command": ["cargo", "clippy", "--quiet"],
"tool_filter": ["write_file", "edit_file"], "path_filter": ["**/*.rs"],
"requires_bin": "cargo" }
] } }At runtime the secret-guard (from defaults) runs first, then the profile's linter — both fire.
Skills are the agent's plug-in tools. They are configured server-side (loaded
by octos serve from the profile and its skill directories); the TUI surfaces
them through /skills when the server advertises the capability. A skill is a
directory containing a manifest.json and an executable binary.
greeter/
├── manifest.json # declares the skill id, its tools, and load gating
└── main # the executable (chmod +x); override the name with "binary"
manifest.json declares the skill and each tool it exposes:
{
"name": "greeter",
"version": "1.0.0",
"author": "you",
"description": "Friendly greetings for any name",
"binary": "main",
"timeout_secs": 10,
"tools": [
{
"name": "greet",
"description": "Return a greeting for a person by name.",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Who to greet" }
},
"required": ["name"]
}
}
],
"requires": { "bins": [], "env": [], "os": [] }
}| Field | Meaning |
|---|---|
name / id |
Skill identifier (kebab-case). Equals the directory name and the id used by the layering rules below. |
version |
Semver string. |
binary |
Executable filename relative to the skill dir (default main). |
timeout_secs |
Per-tool-call timeout. |
tools[] |
One entry per tool: name (snake_case, unique), description, input_schema (JSON Schema). Add "concurrency_class": "exclusive" to a tool that writes files or mutates shared state so the scheduler never races it against a sibling. |
requires |
Load gating: bins (must be on PATH), env (must be set), os (allowed values; empty = any). |
The runtime invokes ./<binary> <tool_name>, writes the tool arguments as JSON
to stdin, and reads one JSON object from stdout:
#!/usr/bin/env bash
# greeter/main — implements the `greet` tool
set -euo pipefail
tool="$1" # tool name = argv[1]
args="$(cat)" # JSON arguments on stdin
name="$(printf '%s' "$args" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("name","world"))')"
case "$tool" in
greet) printf '{"success": true, "output": "Hello, %s!", "files_to_send": []}\n' "$name" ;;
*) printf '{"success": false, "output": "unknown tool: %s"}\n' "$tool" ;;
esacThe response object is { "output": string, "success": bool, "files_to_send": [paths] }. output is what the model sees; any paths in files_to_send are
auto-delivered to the chat.
octos serve scans, in order: the project-local plugins/ and skills/
directories, the bundled system skills, per-profile installs under
<data-dir>/skills/, and any colon-separated paths in OCTOS_SKILLS_PATH. Drop
the greeter/ directory into your project's skills/ (or the profile's
<data-dir>/skills/) and restart the server. The legacy global
~/.octos/skills and ~/.octos/plugins directories are deprecated and no
longer scanned.
Every deployment ships a set of bundled system skills — the "super power" skills. The core app skills:
Skill (id) |
What it does |
|---|---|
weather |
Current weather + multi-day forecast for any city via Open-Meteo (no API key). |
clock |
Current date/time in any timezone (directory time/). |
news |
Raw headlines and article text from Google News, Hacker News, Yahoo News, Substack, and Medium. |
deep-search |
Iterative multi-round web research: parallel crawling, reference chasing, structured report. |
deep-crawl |
Recursive same-origin website crawl via headless Chrome (requires google-chrome). |
send-email |
Send email via SMTP or Feishu/Lark Mail. |
account-manager |
Manage sub-accounts under the current profile. |
The voice platform skill (OminiX ASR + preset-voice TTS on Apple Silicon) is
admin-only and loaded explicitly by octos serve.
A profile can choose which discovered skills load, via a skills block. In a
per-profile file it lives under config.skills; in the global defaults file it
is top-level (see the Hooks section for the two placements and how they
merge). Omitting the block loads every discovered skill — the default,
backward-compatible behavior.
{
"skills": {
"mode": "all_discovered",
"rules": [
{ "id": "deep-crawl", "enabled": false }
]
}
}mode: "all_discovered"(the default) loads every discovered skill except those with anenabled: falserule. The example above ships everything butdeep-crawl.mode: "all_list"loads only skills with an explicitenabled: truerule — everything else is disabled, including the bundled system skills. Use it to pin a profile to a fixed toolset:
{
"skills": {
"mode": "all_list",
"rules": [
{ "id": "weather", "enabled": true },
{ "id": "clock", "enabled": true }
]
}
}Rules are keyed by the manifest id and are last-wins per id. When the skills
block is inherited from profile-defaults.json, the two rule sets are unioned
(defaults first) and the profile's rule for a given id replaces the inherited
one — so a profile can re-enable a skill the global defaults disabled:
// ~/.octos/profile-defaults.json — top-level "skills", inherited by all profiles
{ "skills": { "mode": "all_discovered", "rules": [ { "id": "deep-crawl", "enabled": false } ] } }// ~/.octos/profiles/research.json — re-enables deep-crawl for just this profile
{ "config": { "skills": { "rules": [ { "id": "deep-crawl", "enabled": true } ] } } }| Symptom | Fix |
|---|---|
octos-core dependency not found |
Keep octos and octoscode as sibling directories. |
| Welcome screen never appears | Use a fresh empty --data-dir and omit --profile-id. |
| Endpoint rejected | Use a ws:// or wss:// URL; HTTP URLs are rejected. |
| Auth failure | Use the same token on octos serve --auth-token and the TUI (--auth-token or OCTOS_AUTH_TOKEN). |
| TUI opens but no live answer | Confirm the server has a provider/model/key and restart it after config changes. |
| Wrong workspace | Start octos serve with the desired --cwd. |
can't find terminfo database |
Set TERM=xterm-256color or install terminfo on the host. |
| Raw logs/timestamps in the UI | Start both server and TUI with RUST_LOG=off. |
target lock or permission error |
Run with CARGO_TARGET_DIR=/tmp/octoscode-target. |
Run the complete unit/integration suite (mock-backed, no server needed):
cargo test --all-targets
# CARGO_TARGET_DIR=/tmp/octoscode-target cargo test --all-targets # on shared/locked hostsHeavier live and visual harnesses live alongside the code:
scripts/run-onboarding-tmux-soak.sh— reference end-to-end onboarding flow: starts a server, launches the TUI, and waits for the "Welcome to Octos" splash. Seedocs/ONBOARDING_TMUX_SOAK.md.- The tmux AppUi smoke and live Codex-parity harnesses live in the sibling
octosrepo (they start both the server and the TUI); point them at this repo withOCTOSCODE_DIR="$PWD/../octoscode".
For release packaging, pin octos-core to the matching Octos git tag or
published crate version instead of the sibling path.
octoscode consumes Octos UI Protocol fields from octos-core and must not
invent local wire extensions. Any protocol change must land through a formal UI
Protocol change request with shared types, server tests, golden protocol tests,
and TUI reducer/rendering tests.
In protocol mode the TUI requests pane.snapshots.v1 and hydrates optional pane
data from session/open.panes when the server supports it, falling back to
session snapshots, task tails, launch target, and status otherwise.
Auth, onboarding, and profile LLM provider setup are governed by UPCR-2026-016
in the octos repo. The TUI consumes auth/*, profile/local/create,
profile/llm/*, and config/capabilities/list as server-owned AppUI methods
over WebSocket or stdio; it never hard-codes provider/model truth or persists a
parallel LLM registry. The current v1 bridge stages selected diff context as
prompt text — structured context attachments are tracked in
docs/M9_31_CONTEXT_ATTACHMENTS_UPCR.md.