jcode (1jehuang/jcode) joins the CLI registry as the `jcode` run mode:
Run menu, phone overview, TUI, quick-start and POST /api/sessions
(`jcodeConfig: { model?, resumeSessionId? }`), local, SSH and Docker
(dedicated agent-image layer), plus codeman doctor and install.sh
through the generated catalogue. Measured on a live jcode 0.98.1 pane.
- Work detection reads jcode's braille status row and `<n>>` composer;
alt screen with mouse strip and PageUp/PageDown paging like opencode.
- Every jcode TUI is a client of one shared per-user daemon whose tools
run with the daemon's env, so no agent skill injection and no JCODE_*
envOverrides prefix.
- New `capabilities.sharedDaemon`: a session kill spares descendants
outside the pane's process group, so closing the tab that started the
daemon no longer cuts every other jcode session off (measured).
- last-response answers an honest empty for an external CLI with no
transcript reader that the pane segmenter cannot read (jcode alone).
- GET /api/jcode/status via the generic registry resolver.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
16 KiB
jcode sessions
Codeman can drive jcode (1jehuang/jcode, MIT, a
coding-agent TUI written in Rust) as a session backend, alongside Claude Code, OpenCode,
Codex, Gemini, Antigravity, Pi, Grok, DeepSeek and OMP. jcode is its own run mode:
its own PTY, its own tmux session, its own tab identity (amber, JC badge, a plain ring
standing in for jcode's halftone donut). It is not a location overlay like Docker or
remote-SSH cases, and it is not a web tab.
Everything here was measured against jcode v0.98.1 on 2026-10-16 (see
How it was measured). The registry entry in
src/config/cli-registry/stock.ts carries the same facts in its comments and is the
source of truth when the two disagree.
Install
curl -fsSL https://jcode.sh/install | bash
The installer downloads a checksummed release binary into
~/.jcode/builds/versions/<version>/ and links ~/.local/bin/jcode (its
JCODE_INSTALL_DIR default) to it. The linked jcode is a small sh launcher that
resolves its own path and execs the jcode-<os>-<arch>.bin beside it, so the version
directory is what matters, not the single file the link points at.
Codeman resolves the binary through the generic registry resolver (there is no
jcode-specific resolver module): the server PATH, then ~/.local/bin, /usr/local/bin
and ~/bin, then an interactive login shell. npm carries an unrelated package also called
jcode, so a PATH hit is not trusted on its own: jcode --version must print
jcode v<semver> (the real one prints jcode v0.98.1 (1c7426eb1)), and anything else
counts as absent. Check what it resolved:
curl -s localhost:3000/api/jcode/status | jq
# { "available": true, "path": "/home/you/.local/bin", "version": "0.98.1" }
Authenticate
jcode keeps its own logins: OAuth accounts in ~/.jcode/auth.json, API keys in
~/.config/jcode/<provider>.env, the default provider and model in ~/.jcode/config.toml.
Set them up with jcode itself (jcode login, /login inside the TUI, or
jcode provider add <name> --base-url ... --model ... for an OpenAI-compatible server).
jcode can also offer to reuse logins other CLIs left behind; it asks before reading any of
them. Codeman forwards no credentials into a jcode pane.
What Codeman wires up
JcodeConfig (per session, persisted in state.json, round-trips through respawn and
server restart):
| Field | Flag | Notes |
|---|---|---|
model |
--model <v> |
e.g. claude-opus-4-8, gpt-5.5 |
resumeSessionId |
--resume <v> |
a session id (session_fox_1792160401272_f5b47ce47d631006) or its short name (fox) |
Both values are regex-validated at the route and checked again by the argv engine, and a
value that fails is dropped, never escaped into the command. resumeSessionId uses the
path-segment pattern (a leading letter or digit) on purpose: jcode's --resume takes an
OPTIONAL value (a bare --resume opens its session picker), so a value starting with -
would be parsed as a flag of its own (--resume --no-update is two flags).
The Run button sends no jcodeConfig: jcode has no approval prompts and no bypass
flag, so there is nothing to opt into (the same shape as pi). A bare spawn is just jcode.
There is no envOverrides prefix for jcode: JCODE_* is not allowlisted. See
The shared daemon for why.
The shared daemon
Every jcode TUI is a CLIENT of one shared per-user daemon (jcode serve). The first client
starts it, in its own session and process group (setsid), and every later client attaches
to it. It stays the first client's CHILD by parent pid, though, which is what the
session kill has to account for. The agent and its tools run in the DAEMON, with the daemon's
environment. Measured: a second client started with CODEMAN_PROBE=second-client asked its
bash tool to echo "probe=[$CODEMAN_PROBE]" and got probe=[].
Three things follow, and each one is a deliberate decision:
- The agent skill is not injected. jcode does read
./.agents/skills(and./.jcode/skills,./.claude/skills), but a jcode agent's tools see theCODEMAN_SESSION_ID,CODEMAN_API_URLandCODEMAN_MUXof whichever session started the daemon, or none at all if it was started outside Codeman. An injected skill would act as THAT session: spawn children under the wrong parent, prompt another session's workers. A user-level copy of the skill (~/.agents/skills, which jcode also reads) has the same problem; this is the same trap as codex's sharedapp-server, only it is jcode's normal case rather than an edge. - No
JCODE_*envOverrides prefix. Most of that namespace is read by the daemon, so a per-session value would do nothing, and where it did act (in the session that happens to start the daemon) it would reach every jcode session on the machine. The namespace includes variables that run code or redirect traffic:JCODE_HOOK_*,JCODE_SPAWN_HOOK,JCODE_*_COMMAND,JCODE_*_API_BASE. Configure jcode in~/.jcode/config.toml, or in the Codeman server's own environment. - Deleting a Codeman jcode session leaves the daemon running, by design: the other
sessions live there (next section).
jcode server stop --forcestops it by hand.
Closing a tab never takes the daemon down
Codeman's session kill signals every descendant of the pane (by parent pid), so it used to
kill the shared daemon whenever the closed tab happened to be the one that started it.
Measured on a beta before the fix: two jcode sessions, the first one deleted, and the second
one's client looped on Connection lost - retrying (attempt 9) - server closed the connection with its prompt queued (⏳), while a send-and-wait on it timed out.
The jcode entry declares capabilities.sharedDaemon: true, and for such a CLI the kill
signals only the descendants still in the pane's own process group
(descendantsInPaneGroup(), src/proc-tree.ts, one bounded ps -o pid=,pgid=). The daemon
left that group when it called setsid, and everything it runs inherits its group, so the
daemon and its tools are spared while the client and anything else in the pane's group are
killed as before. A lookup that fails keeps the old behaviour (kill every descendant). Every
other CLI is unaffected. Measured after the fix: the kill logs Sparing 1 process(es) outside pane <pid>'s group (the CLI's shared daemon), the daemon keeps its pid, and the
remaining session answered a send-and-wait in 5 s.
The flip side: a process a jcode agent started (a dev server, a watcher) is a child of the daemon, not of the pane, so closing the tab does not stop it. That is how jcode behaves in a plain terminal too.
Approvals and multi-user mode
jcode has no approval dialog. Its bash tool runs ungated apart from jcode's own model-side
reflection gate on risky commands and a hard deny on catastrophic paths. So there is no
bypass flag to clamp, nothing for the multi-user clamp to force off, and no dialog for
Codeman to read as blocked. In multi-user mode a jcode session is like a pi session: an
agent that runs tools without asking, under the one OS account every session shares.
Terminal behavior
jcode is a full-screen alternate-screen TUI with mouse tracking on (tmux
alternate_on=1, mouse_any_flag=1, mouse_sgr_flag=1). It takes the opencode strip,
altScreen: 'strip-mux-and-mouse': the mouse DECSETs are stripped, so a drag selects text
locally (Auto Copy and copy-on-select work) instead of being reported to jcode.
Scrolling: jcode pages its own transcript on PageUp/PageDown and also scrolls on SGR wheel
reports (both measured). It declares transcriptPageKeys: true, so over the hollow
alt-screen buffer a wheel or a swipe becomes PageUp/PageDown instead of doing nothing.
Input: a long prompt sent the way Codeman sends one (a bracketed paste, then a separate Enter) submits cleanly (measured with a 170-character prompt).
How Codeman reads a working pane
At rest the composer row is <n>> (the prompt number, then >); a submitted prompt is
echoed into the transcript as <n>› <text>. While a turn runs the composer row reads
<n>… and a status row appears directly above it: a braille spinner (the gemini/omp frame
set, about 12 fps), then the phase and the elapsed time:
⠋ sending… 0s
⠦ thinking… 2s · https
⠼ waiting for response… 3s · https
⠸ running bash · <detail> · 12s · Alt+B bg
⠼ 12s · 45.1 tps · ↑16k ↓20 (streaming: the time comes first)
↻ network disconnected, waiting to retry · … · 5s
Past a minute the time reads 1m 3s, past an hour 1h 2m. At rest that row is empty or a
dim tip, never a spinner, and the turn leaves a summary row ( 21s · 199.6 tps · ↑17k ↓2k)
that has a time but no spinner. The workingLine pattern therefore needs a spinner (or the
↻ network disconnected row) at the start of a row AND an elapsed time on it.
The composer glyph is the bare >. tmux redraws only the cells that changed, and on the
composer row at the end of a turn that is the … turning into > (the recorded client
stream carries \x1b[43;2H...> ), never the prompt number, so > is what reliably arrives.
Mid-turn stream chunks never contain it. The glyph only ARMS the idle check; the screen and
the silence decide.
Silence is a sound signal for jcode: the spinner repaints the pane at least every 100 ms for the whole turn, and at rest jcode sends nothing at all (12 seconds recorded: zero bytes).
The submit verifier stays inert for jcode, which is what it wants (it has no early-Enter
defect): every composer row starts with its prompt number, so no screen row starts with
> the way the verifier requires.
Reading a jcode worker's answer
GET /api/sessions/:id/last-response returns an honest empty (text: "", and
messages: [] with ?context=full) for jcode. The pane segmenter the other TUIs use cannot
read a repainting alternate-screen stream (measured: it served jcode's welcome banner as
"the answer"), and without this the route would fall through to the Claude transcript scan,
which could adopt a Claude conversation from the same folder. Read a jcode worker's screen
instead:
codeman agent read <id> --tail 3000- MCP
read_outputwithsource: "terminal" GET /api/sessions/:id/terminal?tail=3000
There are no stop/blocked hook signals for jcode: wait for idle, or use wait-output
markers, as with every hook-less CLI.
First run: the onboarding wizard
A fresh jcode install opens an onboarding wizard ("Welcome to jcode onboarding / Log in to
OpenAI?") instead of the composer, on every launch, until its launch counter passes 5 or
10 sessions exist. A prompt typed into the wizard is lost (measured: it wedged that client).
Before driving jcode from Codeman, run jcode once in a terminal and finish or skip the
wizard, or press Esc in the Codeman pane.
jcode's own defaults Codeman does not change
- Telemetry is on by default in jcode (install count, version, OS, session activity,
tool counts; jcode's
TELEMETRY.md). Opt out withjcode telemetry disable, or setJCODE_NO_TELEMETRY=1in the Codeman SERVER's environment (it is not a per-session env override, see above). - Auto-update is on by default for release builds;
--no-updateis jcode's own flag and Codeman does not pass it. - Self-dev mode: jcode switches into it when started inside a checkout of jcode itself.
Docker cases
jcode is not on npm, so the agent image installs it in a dedicated Dockerfile step through
jcode.sh's installer, then copies the WHOLE version directory to /opt/jcode (the launcher
execs the binary beside it, so copying only the linked file, as the grok and omp steps do,
would leave a launcher with nothing to run) and links /usr/local/bin/jcode to the launcher.
Root's ~/.jcode is dropped in the same layer so the image does not carry the ~160 MB
binary twice. Verified in node:22-bookworm-slim, including jcode --version as the
unprivileged agent user. Rebuild with the mandatory --no-cache:
node scripts/build-agent-image.mjs --no-cache
No credentials are seeded into the container: ~/.jcode holds the OAuth tokens beside the
builds, sessions and SQLite caches, and API keys live in ~/.config/jcode. Sign in inside
the container instead.
Remote SSH cases
jcode mode is routed through an interactive login shell
(exec "$SHELL" -i -l -c 'jcode'), because sshd's remote-command PATH does not include
~/.local/bin. Per-session config does not cross ssh and is rejected rather than silently
ignored; a host whose jcode lives elsewhere takes the per-host commands.jcode override.
Not supported yet, and why
- A transcript reader (a real
last-response, Response Viewer content). jcode writes sessions as~/.jcode/sessions/session_<name>_<ms>_<hex>.jsonplus a.journal.jsonl, which is easy to parse, but pairing a Codeman pane with ITS session file is ambiguous: the session JSON'slast_pidis the shared daemon's, not the pane's, so two jcode panes in one folder cannot be told apart from the files alone. - Custom Model Endpoint Profiles (
customModelInjection: unsupported). jcode's provider is resolved in the daemon, not the pane, so a per-session redirect has nothing reliable to land on. - The model in tile headers (
modelDetect). The status bar names provider and model (qwen5090 qwen3.8 27b), but its layout varies by provider and width and was not measured enough to anchor a pattern on. A session shows its launch model, if any. - Hook-based
stop/blockedsignals. jcode has lifecycle hooks (session_start,turn_start,turn_end,session_end), but they run in the daemon too, with the daemon's environment, so they cannot tell Codeman which session they belong to. - Ralph, respawn heuristics, token/CLI-info parsing and the
❯readiness probe are off for jcode, as for every external CLI.
A per-session daemon (jcode --socket <path>) would give each pane its own environment,
but it gives up the shared daemon that is jcode's main design point (cross-session swarm
awareness, a few MB per extra session), so it was not done here.
How it was measured
- jcode installed with its own installer into a scratch
HOMEwith telemetry off (DO_NOT_TRACK=1,JCODE_NO_TELEMETRY=1) and a shortXDG_RUNTIME_DIR(the daemon's Unix socket path must stay under theSUN_LENlimit), so nothing touched the real home. - A provider set up with
jcode provider addpointing at a local OpenAI-compatible server (an RTX 5090 serving Qwen 3.8 27B), so no subscription login was involved. - jcode run in a scratch tmux socket at 160x45;
tmux capture-pane -pevery 200 ms through real turns (a plain reply, a bash tool call, a streamed answer);pipe-paneat rest; and a tmux CLIENT attached through a PTY to record exactly what Codeman's PTY reads. - PageUp/PageDown sent with
send-keys; SGR wheel reports (\x1b[<64;40;10M) pasted raw. - A second client with its own environment to prove where tools run.
- The Docker step run in a throwaway
node:22-bookworm-slimcontainer. - End to end through an isolated beta Codeman built from this branch (its own
CODEMAN_INSTANCE, port and tmux socket):POST /api/quick-startwithmode: "jcode",/api/jcode/status, send-and-wait (wait: "idle"returned after the turn, 12.7 s for a turn with a 5 s tool call, not during it), two sessions with the first deleted, and a Playwright pass over the Run menu entry, theRun JCbutton and the rendered transcript.
The fixtures in test/jcode-mode.test.ts are verbatim from these captures.