Files
Codeman/docs/pi-integration.md
Codeman maintainer c5b59633d8 feat(pi): add Pi (pi.dev) as a sixth CLI run mode (#206)
SessionMode gains 'pi', a first-class backend alongside Claude Code,
OpenCode, Codex, Gemini and Antigravity: its own PTY, tmux session, rose
tab identity, welcome button, run-mode entry, cron agentType, Docker and
remote-SSH command defaults, and clone-repo Brain option.

Pi is a different shape of CLI from the other four, and three decisions
follow from that:

- It has NO permission prompts and no sandbox, so there is no
  --dangerously-skip-permissions analog and none was invented. The
  privilege-shaped knob is the tri-state approveProjectTrust, which makes
  pi load and EXECUTE repo-local .pi/extensions TypeScript and install
  missing project packages. clampExternalCliBypassForOwner() therefore
  puts pi in the MATERIALIZE branch: a non-granted multi-user owner gets
  --no-approve even when no config was sent, because pi's own default is
  a prompt the session user could answer themselves. That helper had zero
  test coverage; it now has coverage for all four CLIs.
- Only the PI_ prefix joins the env allowlist. Pi's ~34 provider key vars
  share no prefix and ALLOWED_ENV_PREFIXES is one global list with no mode
  context, so admitting them would widen the allowlist for every mode at
  once. Auth goes through pi's /login or the server's own environment.
  --api-key is deliberately never wired: it would put a provider secret on
  the spawn command line.
- pi stays OUT of isAltScreenStripMode(). Its default TUI renders into the
  main screen with terminal-owned scrollback, and its 0.84.0 fullscreen
  mode is runtime-switchable via /settings; that flip was measured to put
  the pane into the alt screen, which the strip would have corrupted.

pi-cli-resolver.ts additionally sanity-probes `pi --version` and requires
semver-shaped output, because `pi` is a short generic name a stray binary
can shadow; GET /api/pi/status surfaces path and version so a
misresolution is diagnosable rather than presenting as a broken mode.

Docker installs pi in its own --ignore-scripts step so that flag cannot
affect the other four CLIs, and seeds its credentials per-file rather than
whole-dir (~/.pi/agent also holds sessions, extensions and package trees).

Verified end to end against pi 0.84.1 on an isolated instance: resolver
search-dir fallback, flag construction, piConfig persistence across a full
server restart, the trust prompt and its --no-approve suppression, the
rose Run button on the default daylight-blue skin (the nested skin block
eats per-mode gradients unless the rule lives inside it), and the buffer
local-echo policy, which pi tolerates where codex did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 13:54:47 +02:00

12 KiB
Raw Permalink Blame History

Pi (pi.dev) sessions

Codeman can drive Pi (@earendil-works/pi-coding-agent, MIT) as a session backend, alongside Claude Code, OpenCode, Codex, Gemini and Antigravity. pi is a sixth run mode: its own PTY, its own tmux session, its own tab colour (rose). It is not a location overlay like Docker or remote-SSH cases, and it is not a web tab.

Tracking issue: #206. The design rationale behind each decision below lives in docs/pi-integration-plan.md.

Install

npm install -g --ignore-scripts @earendil-works/pi-coding-agent
# or
curl -fsSL https://pi.dev/install.sh | sh

Both installers end up going through global npm, so either one uninstalls with npm uninstall -g @earendil-works/pi-coding-agent.

Codeman finds the binary via which pi and then the usual global-bin locations (~/.local/bin, /usr/local/bin, ~/.bun/bin, ~/.npm-global/bin, ~/bin).

pi is a short, generic name, so unlike the other CLI resolvers Codeman does not trust a which hit on its own: it runs pi --version once and requires semver-shaped output. Anything else is rejected as "not installed" and the rejected path is logged. Check what it resolved:

curl -s localhost:3000/api/pi/status | jq
# { "available": true, "path": "/home/you/.local/bin", "version": "0.84.1" }

That endpoint carries version on top of the shape the sibling /api/*/status endpoints return, precisely so a misresolution is visible rather than presenting as "the mode just doesn't work".

Authenticate

Pi supports 15+ providers. Two ways in:

  • OAuth subscription login — run /login inside a pi session. Six providers support it: ChatGPT Plus/Pro, Claude Pro/Max, GitHub Copilot, xAI, OpenRouter and Radius. Credentials land in ~/.pi/agent/auth.json and pi refreshes them itself. OpenRouter's flow accepts a pasted redirect URL, which is what makes it workable over remote SSH.
  • API keys — exported in the environment of the Codeman server process.

⚠️ Provider API keys cannot be sent as per-session envOverrides. Pi reads about 34 provider variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, HF_TOKEN, BASETEN_API_KEY, …) that share no common prefix. Codeman's env allowlist is a single global list applied to every mode at once, so admitting bare provider keys for pi would widen the allowlist for Claude, Codex, Gemini and everything else too. Only the PI_* prefix was added, which covers every documented pi input: PI_CODING_AGENT_DIR, PI_CODING_AGENT_SESSION_DIR, PI_PACKAGE_DIR, PI_OFFLINE, PI_SKIP_VERSION_CHECK, PI_TELEMETRY, PI_CACHE_RETENTION, PI_SHARE_VIEWER_URL, PI_HARDWARE_CURSOR, PI_EXPERIMENTAL.

pi auth check verifies credentials before you start a long run.

Note if you authenticate with a Claude Pro/Max subscription: third-party harness usage bills as Anthropic "extra usage" per token rather than against plan limits.

What Codeman wires up

PiConfig (per session, persisted in state.json, round-trips through respawn):

Field Flag Notes
model --model <v> Accepts provider/id and a :<thinking> suffix (sonnet:high)
provider --provider <v> anthropic, openai, google, …
thinking --thinking <v> off/minimal/low/medium/high/xhigh/max
continueSession -c Skipped when resumeSessionId is set (the two conflict)
resumeSessionId --session <v> Ids only, never paths
approveProjectTrust --approve / --no-approve / nothing Tri-state, see below

Every value is regex-validated and dropped (not escaped) if it fails, because the result is interpolated into the pane's bash -c "…" command.

The Run button sends no PiConfig at all: pi has no permission prompts to bypass, and project trust is a decision the person at the terminal makes.

What Codeman deliberately does NOT wire up

  • --api-key. Never. It would put a provider secret on the spawn command line, visible in ps, tmux server state and logs. PI_* overrides go through socket-scoped tmux setenv for exactly this reason.
  • --tui-mode. Pi's default main-screen TUI is the friendly case for a browser terminal. The fullscreen mode (0.84.0) stays your own runtime choice via /settings.
  • --name, --no-session, -p/--print, --mode json, --mode rpc, --tools/--exclude-tools, -e/--extension, --skill, --system-prompt. Tracked as follow-ups in the plan doc.

Permission and trust model — read this

Pi has no permission prompts and no sandbox. There is no --dangerously-skip-permissions analog and none is needed: tools run with the user's own permissions, always. A pi session can read, write and execute anything the Codeman user can. If you need isolation, use a Docker case — that is the isolation story, here as everywhere else in Codeman.

Pi's "project trust" prompt is not a safety boundary (upstream says so too). It gates loading repo-local .pi/ config, extensions and skills, and installing missing project packages. It only appears when the cwd or an ancestor contains .pi/settings.json, .pi/extensions|skills|prompts|themes, .pi/SYSTEM.md/.pi/APPEND_SYSTEM.md, or .agents/skills. A bare .pi/ directory does not trigger it.

approveProjectTrust: true answers it with --approve, which means pi loads and executes repository-supplied TypeScript and runs an npm install for missing project packages. Treat it exactly as seriously as that sounds.

Multi-user mode: for an owner without the privileged-command grant, Codeman materializes approveProjectTrust: false so the pane launches with --no-approve and the prompt never appears. Merely omitting --approve would not be a clamp, since pi's own default is to ask and the session user could just answer yes.

Also worth knowing: pi auth print-api-key / print-bearer-token and pi auth check mean a pi session can print its own provider credentials by design. Isolation is Docker.

tmux extended keys (Shift+Enter)

Pi's editor uses Shift+Enter / Ctrl+Enter for newline-vs-submit. Without extended keys, tmux collapses both into a plain \r. Upstream recommends:

set -g extended-keys on
set -g extended-keys-format csi-u

extended-keys-format needs tmux 3.5+; on 3.2–3.4 extended-keys on alone works (pi falls back to xterm modifyOtherKeys).

Codeman's browser input path sends \r for submit, so basic use works unconfigured — what degrades is newline-in-editor, mostly when you attach to the pane directly (sc).

⚠️ Upstream notes the setting may need a full tmux kill-server to take effect. Never run tmux kill-server on Codeman's socket — it would kill every live session, w1/w2/w3 included.

Measured (tmux 3.4, pi 0.84.1): no kill-server is needed. Setting the option server-scoped on Codeman's own socket takes effect on the ALREADY-RUNNING server; the next pi session starts without the warning. Existing sessions keep the old setting until they respawn.

tmux -L codeman set -s extended-keys on
tmux -L codeman set -s extended-keys-format csi-u   # tmux 3.5+ only, see below
tmux -L codeman show-options -s | grep extended     # verify

On tmux 3.4 and older, extended-keys-format does not exist and the second line fails with invalid option: extended-keys-format. That is harmless — pi falls back to xterm modifyOtherKeys and extended-keys on alone silences the warning. Run the two lines independently rather than chained.

Pi tells you which state it is in: an unconfigured session prints Warning: tmux extended-keys is off. Modified Enter keys may not work. in its startup banner, so you can verify the change by starting a new pi session.

⚠️ Use -L <socket> and -s, never -g on your default socket, and never kill-server. Codeman does not set this for you: it is a server-wide tmux option and silently changing key encoding for every session of every backend is not Codeman's call to make.

Typing from the browser (local echo)

On touch devices Codeman buffers typed characters in the LocalEchoOverlay and flushes them to the PTY on Enter. Pi gets that 'buffer' policy, the same as Claude, Gemini and OpenCode.

This was an explicit open question, because that policy is exactly what broke Codex (issues #218/#219/#220/#222): Codex's composer reacts per keystroke, so buffer-until-Enter starved it. Measured against pi 0.84.1: it does not reproduce. Pi's slash-command picker re-filters on the whole composer content rather than on per-keystroke deltas, so a one-shot flush of /set filters the picker down to settings identically to typing it character by character, and the delayed \r then selects it. Prose prompts flush and submit correctly too.

If a future pi release changes that, the cheap fallback is one 'off' branch in _updateLocalEchoState (terminal-ui.js); teaching PredictiveEchoAddon pi's composer row is the larger follow-up.

Docker cases

The agent image (docker/agent.Dockerfile) installs pi in its own RUN step with --ignore-scripts, kept out of the shared npm block so the flag cannot change how the other four CLIs install. Rebuild with:

node scripts/build-agent-image.mjs --no-cache   # --no-cache is mandatory

Credentials are seeded, not shared: ~/.pi/agent/auth.json, settings.json, trust.json, models.json and models-store.json are mounted read-only and copied into the container's own ~/.pi/agent. So an in-container pi never writes refreshed OAuth tokens back to the host, and docker commit exports stay secret-free. models.json is in the list because it holds user-defined custom providers, which would otherwise silently vanish inside containers.

Only those five files are seeded because ~/.pi/agent also holds sessions/, extensions/, skills/ and the installed package trees (npm/, git/), which on an active host is easily gigabytes.

Trade-off: in-container pi sessions are invisible host-side, so pi -c inside a Docker case only sees that container's own history.

Remote SSH cases

pi mode is routed through an interactive login shell (exec "$SHELL" -i -l -c 'pi'), because sshd's remote-command PATH does not include npm's global bin on most hosts. Per-session config and envOverrides do not cross ssh and are rejected rather than silently ignored; use the per-host command override instead.

Known gaps

  • No idle/completion hook. Pi has no hook system Codeman can install into, so idle detection falls back to output-stabilization like the other external CLIs. Pi 0.84.0 shipped an agent_settled extension event that is a genuine idle signal; a Codeman pi extension using it is the highest-value follow-up.
  • No response viewer. Pi writes JSONL v3 session files under ~/.pi/agent/sessions/; nothing reads them yet.
  • Cron jobs mis-detect readiness. The cron readiness poll looks for ❯ or a token count, neither of which pi prints, so a pi cron job burns its poll budget and then sends the prompt anyway. It works; it is just slower to start.
  • Ralph, respawn heuristics, token/CLI-info parsing and the ❯ readiness probe are off for pi, as for every external CLI.