# jcode sessions Codeman can drive [jcode](https://github.com/1jehuang/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](#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 ```bash curl -fsSL https://jcode.sh/install | bash ``` The installer downloads a checksummed release binary into `~/.jcode/builds/versions//` 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--.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` (the real one prints `jcode v0.98.1 (1c7426eb1)`), and anything else counts as absent. Check what it resolved: ```bash 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/.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 --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 ` | e.g. `claude-opus-4-8`, `gpt-5.5` | | `resumeSessionId` | `--resume ` | 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](#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](#closing-a-tab-never-takes-the-daemon-down) 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 the `CODEMAN_SESSION_ID`, `CODEMAN_API_URL` and `CODEMAN_MUX` of 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 shared `app-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 --force` stops 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 '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 `> ` (the prompt number, then `>`); a submitted prompt is echoed into the transcript as `› `. While a turn runs the composer row reads `…` 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 · · 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 --tail 3000` - MCP `read_output` with `source: "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 with `jcode telemetry disable`, or set `JCODE_NO_TELEMETRY=1` in 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-update` is 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`: ```bash 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___.json` plus a `.journal.jsonl`, which is easy to parse, but pairing a Codeman pane with ITS session file is ambiguous: the session JSON's `last_pid` is 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`/`blocked` signals.** 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 `) 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 `HOME` with telemetry off (`DO_NOT_TRACK=1`, `JCODE_NO_TELEMETRY=1`) and a short `XDG_RUNTIME_DIR` (the daemon's Unix socket path must stay under the `SUN_LEN` limit), so nothing touched the real home. - A provider set up with `jcode provider add` pointing 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 -p` every 200 ms through real turns (a plain reply, a bash tool call, a streamed answer); `pipe-pane` at 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-slim` container. - End to end through an isolated beta Codeman built from this branch (its own `CODEMAN_INSTANCE`, port and tmux socket): `POST /api/quick-start` with `mode: "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, the `Run JC` button and the rendered transcript. The fixtures in `test/jcode-mode.test.ts` are verbatim from these captures.