Skip to content

octoscode

██████╗ ██████╗████████╗ ██████╗ ███████╗ ██╔═══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔════╝ ██║ ██║██║ ██║ ██║ ██║███████╗ ██║ ██║██║ ██║ ██║ ██║╚════██║ ╚██████╔╝╚██████╗ ██║ ╚██████╔╝███████║ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝

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.

Start here

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:

octoscode

On 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 mock opens a mock demo with canned replies — no server, connected to nothing. Plain octoscode is 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.

If something looks wrong

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.


📦 Install

Every method installs a single self-contained octoscode binary. Then run octoscode --help.

⬇️ Prebuilt binary — no Rust toolchain needed (recommended)

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 source with Cargo (needs Rust 1.85+)

From git — no crates.io publish required

cargo install --git https://github.com/octos-org/octoscode octoscode

From crates.io — once published

cargo install octoscode

octos-core (the shared protocol crate) is pulled automatically as a git dependency, so installing needs no sibling octos checkout.


Quickstart: solo onboarding

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.

1. Install the TUI

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` below

Developing against a local octos? To build against an uncommitted sibling ../octos/crates/octos-core instead of the pinned git revision, run cp .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.

2. First run → the welcome screen

Just run it — the TUI provisions and launches the server for you:

octoscode

You 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 octos server into ~/.octos/bin and 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/ws for a remote one. Do not pass --profile-id on a true first run — it selects an existing profile and skips onboarding.

3. Create your local profile

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>
Email 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.

4. Set up an LLM provider

The screen now reads "Set Up LLM Provider" ("Choose a dashboard model route, enter its API key, then save."). Work down the rows:

  1. Load provider catalog — pulls the dashboard's model families and routes.
  2. Model family → Model → Provider route — pick one route.
  3. API key — select the row and type the key (/onboard key <secret>); it is masked in state, logs, and snapshots.
  4. (optional) Test provider to verify the route.
  5. 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.

5. Open a coding session and chat

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.

Agent permissions & code review

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.


Dual loop: OctoLoop

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.

Set it up (one command)

cd your-project/
curl -fsSL https://raw.githubusercontent.com/octos-org/octoscode/main/scripts/olp-init.sh | bash

It 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.

Hand an agent the card

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.

Drive the inner loop from the outside

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 interrupt

herdr 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.

OctoLoop docs

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.


Other ways to run

Two ways to run

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.

Connect to a running octos serve over WebSocket

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/ws

With --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.

Use the terminal and the browser together, step by step

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 dev

Keep 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 main

The 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.12 can print a one-time pairing link with octos 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/start is 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 --readonly on the TUI when you want it to watch without sending.

Attaching: what differs, and what bites

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-url prints a link for the browser; the TUI has no /pair/claim support, so give it --auth-token or OCTOS_AUTH_TOKEN. The token rides an Authorization: Bearer header 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 /profiles are unavailable; pass --profile-id <id>.
  • --session is 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 _main regardless 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.

Mock mode (no server)

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.


Reference

CLI flags

--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 file

~/.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.

Themes

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).

Startup splash

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-splash or OCTOSCODE_NO_SPLASH=1 turns it off.
  • OCTOSCODE_SPLASH_EFFECT=matrix pins a specific effect (any name from the curated set; unknown names fall back to the random pick).

In-session keys and slash commands

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.

Composer editing

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.

Scrolling and the transcript pager

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.

Markdown rendering

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.

Languages (i18n)

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.

Environment variables

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. Every OCTOS_TUI_* variable is now OCTOSCODE_*. The one exception that still works is OCTOS_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.

Workspace (cwd) behavior

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.

Provider changes after the server is running

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

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.

Placement and inheritance

Hooks (and env_vars, sandbox, plugins, memory, and the skills layer below) can be declared in two places:

  • Per-profile — under config.hooks in ~/.octos/profiles/<id>.json. Fires only for that profile.
  • Globally — as a top-level hooks array 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.

Config shape

{
  "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).

Events

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.

Protocol

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_name and arguments, and (after) result, success, duration_ms. Arguments/results for shell, read_file, and write_file are redacted; other tools are truncated to 1 KB.
  • user_prompt_submit adds the prompt text and the turn's cwd.
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.

Examples

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/profile-defaults.json  — top-level "hooks", inherited by all profiles
{ "hooks": [ { "event": "user_prompt_submit", "command": ["~/.octos/hooks/no-secrets.sh"] } ] }
// ~/.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.

Custom skills

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.

Anatomy

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).

Binary protocol

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" ;;
esac

The 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.

Where skills are discovered

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.

Built-in "super power" system skills

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.

Per-profile skill layering

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 an enabled: false rule. The example above ships everything but deep-crawl.
  • mode: "all_list" loads only skills with an explicit enabled: true rule — 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 } ] } } }

Troubleshooting

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.

Testing and harnesses

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 hosts

Heavier 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. See docs/ONBOARDING_TMUX_SOAK.md.
  • The tmux AppUi smoke and live Codex-parity harnesses live in the sibling octos repo (they start both the server and the TUI); point them at this repo with OCTOSCODE_DIR="$PWD/../octoscode".

For release packaging, pin octos-core to the matching Octos git tag or published crate version instead of the sibling path.


Protocol contract

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.


Community and governance

About

Octoscode - Octos's Coding Agent

Resources

Code of conduct

Contributing

Security policy

Stars

42 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages