Files
Codeman/docs/copilot-integration.md
T
Codeman maintainer 75186a5d97 fix(#592): clamp the COPILOT_* variables that bypass approval, run code or move the token
The COPILOT_ prefix admits every variable Copilot reads, and the 1.0.98
bundle reads far more than `copilot help environment` lists. Only
COPILOT_HOME, COPILOT_ALLOW_ALL and the BYOK trio were privileged, so a
non-granted multi-user owner could still hand back what the allowAll
clamp removed:

- approval: COPILOT_PLAN_THEN_AUTOPILOT, COPILOT_ASSISTED_APPROVAL
- code with no approval row: COPILOT_CLI_DIST_DIR runs <dir>/index.js
  in place of the CLI, the package caches choose the CLI build,
  COPILOT_EDITOR, COPILOT_PROVIDER_API_KEY_COMMAND and
  COPILOT_AHP_HOST_BIN are executed, skills, provider configs and hooks
  are loaded from the named places (the DSH_HOME class)
- token redirects: COPILOT_API_URL and the other API bases receive the
  CLI's sign-in token, which in multi-user mode is the operator's (all
  users share the OS account and ~/.copilot), and the rest of the BYOK
  provider family re-aims a Codeman custom endpoint (the
  DEEPSEEK_BASE_URL / OMP_AUTH_BROKER_URL class)

All are now privilegedEnvKeys. The registry schema capped that list at 8;
the cap is now 48, since a longer deny list only takes more away from
non-granted owners. The test sends one of each class through the real
schema (they all pass it) and the real clamp. The docs say the list is
read from the 1.0.98 bundle and that a variable a later release adds is
not covered until it is listed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 08:04:41 +02:00

11 KiB
Raw Blame History

GitHub Copilot CLI sessions

Codeman can drive GitHub Copilot CLI (copilot, npm @github/copilot) as a session backend, alongside Claude Code, OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek Harness and OMP. copilot is a run mode: its own PTY, its own tmux session, its own tab identity (purple, a drawn mark rather than GitHub's logo). It is not a location overlay like Docker or remote-SSH cases, and it is not a web tab.

Everything here was measured against Copilot CLI 1.0.94 and 1.0.95 inside tmux. The entry itself is registry data (src/config/cli-registry/stock.ts); see cli-registry.md for the field-by-field reference.

Install

npm install -g @github/copilot

CLI management (Settings → Agents & CLIs) can run exactly that for you. Codeman resolves the binary from the server's PATH and the usual npm/nvm prefixes, then runs copilot --version and accepts it only if the output says GitHub Copilot CLI <x.y.z>. A bare copilot on PATH that answers anything else is ignored. Check what it resolved:

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

Authenticate

Codeman never logs Copilot in or stores its credentials.

  • Interactive. Run copilot once and use /login (or copilot login), or do it inside a Codeman tab. The token goes to the OS keychain when one is available.
  • Headless server. There is no keychain on a service, so Copilot offers to keep the token as plain text in ~/.copilot/config.json (authTokens). That is where it ends up when you accept. To keep a token out of a file, set COPILOT_GITHUB_TOKEN in the service environment; Copilot reads it first, then GH_TOKEN, then GITHUB_TOKEN.
  • Per-session override. COPILOT_* is the only prefix the env allowlist admits for Copilot. GH_TOKEN and GITHUB_TOKEN are deliberately not accepted as per-session overrides: the allowlist is one global list with no mode context, so admitting them would make them settable on every session. Use COPILOT_GITHUB_TOKEN.
  • No GitHub sign-in is needed when a session points at your own endpoint (below).

What Codeman wires up

Thing Behaviour
Launch copilot, in Copilot's own Manual Approval mode.
Run button copilot --yolo (allow every tool, path and URL), like the other CLIs' bypass switches, so an unattended session never stops on an approval.
Permission clamp allowAll is a privilegedParam. A multi-user owner without the bypass grant has it forced back to false; an absent config is never materialized (a bare copilot is already safe). COPILOT_HOME, COPILOT_ALLOW_ALL, COPILOT_PROVIDER_* and COPILOT_MODEL are privileged env keys, and so is every other COPILOT_* variable the CLI reads that switches approvals (COPILOT_PLAN_THEN_AUTOPILOT), runs code with no approval row (COPILOT_CLI_DIST_DIR, the package caches, COPILOT_EDITOR, COPILOT_PROVIDER_API_KEY_COMMAND, skills, hooks) or sends the sign-in token to another host (COPILOT_API_URL, COPILOT_GH_HOST, ...): the list is in stock.ts, read from the 1.0.98 bundle. A variable a later Copilot release adds is not covered until it is listed there.
Model --model <id>, sent only by an API caller that sets copilotConfig.model (the value must start with a letter or digit, like resumeSessionId). Nothing in the UI sets it: a saved custom endpoint sets COPILOT_MODEL instead (see below).
Resume --resume <value>, sent only by an API caller that sets copilotConfig.resumeSessionId (Past Sessions does not). The value (an id, an id prefix or a session name) must be one plain word starting with a letter or digit: --resume's value is optional, so --resume --yolo would otherwise make --yolo its own flag and bypass the clamp.
Continue --continue, from Past Sessions: it resumes the most recent conversation Copilot has for that directory. Nothing sets it on a respawn. Skipped when a resume value is set.
Name A tab name you set yourself is passed as --name <tab name>, so Copilot's own session list reads like that tab. An automatic name (w1-myapp, or an auto-named title) is not pinned. Copilot refuses --name with --resume/--continue, so it is dropped there and a continued session keeps the name it had.
Working / idle The footer. A turn shows ◉ Working esc edit prompt (the dot alternates ◉/◎); the composer is the ❯ between two rules. A tool waiting for approval replaces the composer and reads as idle.
Model detection The right-hand field of the footer's last row, at rest and mid-turn.
Status probe GET /api/copilot/status, plus an entry in the CLI-installed probes and codeman doctor.
MCP The entry declares mcpConfig (~/.copilot/mcp-config.json, dialect copilot-json, relocated by COPILOT_HOME), so MCP server sync treats it as a target and a source. copilot mcp disable is honoured (read from settings.json, never written).
Docker The agent image installs @github/copilot. No credential directory is seeded (see below).

Your own endpoint (BYOK)

With custom model endpoints on, a saved endpoint is offered in the Run menu for Copilot and launches straight onto it (no restart; the env is applied before the process starts):

Codeman field Variable
base URL (+ /v1) COPILOT_PROVIDER_BASE_URL
key COPILOT_PROVIDER_API_KEY
model COPILOT_MODEL

COPILOT_PROVIDER_TYPE is not set: it defaults to openai, which is what a Codeman custom endpoint is (llama.cpp, vLLM, Ollama). All of these are privileged env keys.

Checked at the CLI level against a local OpenAI-compatible endpoint (1.0.95): it posted to /v1/chat/completions with the key and the model name, needed no GitHub sign-in, and --resume <id> --model <m> / --continue --model <m> resumed earlier sessions. Tool calls through a custom endpoint were not exercised. Only keys are persisted (never values).

Terminal behavior

Copilot is an alternate-screen TUI with mouse tracking on, so it needed one non-default decision, the same one OpenCode needed:

  • Selection. Left in, xterm reports a plain drag to the TUI instead of selecting, so Auto Copy and copy-on-select silently did nothing (only Shift+drag selected). The entry is altScreen: 'strip-mux-and-mouse': the tracking DECSETs are stripped under tmux, a drag is a local selection, and clicks still reach Copilot through the browser's hand-encoded tap (cliMouseTracking is published as the server strips).
  • Wheel and touch. Stripping the DECSETs also stops xterm encoding the wheel, so _shouldForwardWheelToApp forwards it by hand as SGR reports while Copilot is tracking the mouse; a phone swipe goes through the same gate. Shift+wheel scrolls Codeman's local history. If the tracking flag is stale after a server restart, the gesture falls back to PageUp/PageDown rather than going dead (Copilot pages its own transcript on those keys: three PageUp moved a 140-line answer from lines 109-140 to 010-042).
  • Keys. Keystrokes from tmux send-keys and POST /api/sessions/:id/input reach the composer; Shift+Enter inserts a newline; shell tool calls complete inside tmux (github/copilot-cli#4180 and #4223 report both failing on 1.0.70 to 1.0.74; neither reproduces on 1.0.94).
  • Scrollbar. Copilot draws a ┃ gutter at the right edge of every row; the response reader strips it.

codeman agent verbs

The three CODEMAN_* variables are exported into every pane, so the session-to-session verbs work from inside a Copilot tab and against Copilot workers:

  • spawn waits for the composer. The ready mark is the footer hint / commands · ? help, not ❯: the folder-trust dialog draws ❯ 1. Yes, so the glyph would call a worker ready while it is still asking whether to trust the folder. An untrusted folder therefore makes spawn warn that the composer was not seen instead of claiming ready.
  • send --wait resolves on idle. --until stop is refused (400, "no Claude Code hooks") like every hook-less mode; use idle, exit or a --match marker.
  • read returns the last answer. Copilot's output stream is absolute cursor moves rather than lines, so the route reads the pane's visible screen (tmux capture-pane) and cuts it with a per-CLI pane dialect (src/web/response-viewer-transcript.ts): ❯ <prompt> HH:MM, ● <answer> with indented continuation, and the cwd / Session: N AIC used row, composer, footer and scrollbar as chrome. Everything before the first prompt (banner, trust dialog) is dropped, so a session that has not been asked anything answers nothing.
  • interrupt sends a bare ESC; rm refuses itself.

Docker cases

The agent image installs @github/copilot, and a docker case runs Copilot like any mode. No credential directory is seeded: without a keyring the sign-in token is plain text in config.json, and CRED_STORES (src/docker-hosts.ts) deliberately has no .copilot row. Sign in inside the container (copilot login), or set COPILOT_GITHUB_TOKEN for the session.

Remote SSH cases

A remote or Docker pane runs a bare copilot (defaultRemoteCommandForMode, pinned in test/location-overlay-commands.test.ts), in Copilot's own Manual Approval mode. The copilotConfig flags (--yolo, --model, --resume, --continue) are a local-session feature and are not sent there, so there is nothing for the privileged-param clamp to do. A remote host needs its own copilot on its own PATH and its own sign-in. This path has unit coverage for the built command but was not exercised against a real remote host.

What Codeman deliberately does NOT wire up

  • No login or token storage. See Authenticate.
  • No bypass inferred. --yolo is sent only by the Run button's allowAll, never guessed from anything else, and is clamped for non-granted owners.
  • No --api-key-style flag on the command line. Secrets travel as environment variables through tmux setenv.
  • No Claude-only features. Auto-resume on a usage limit, the Approvals Inbox, Read My Mind, the Ralph tracker, subagent windows and the bundled agent skill are Claude-shaped.

Known gaps

  • agent read does not model tool-call rows: they open with the same ● bullet and read as part of the answer.
  • agent read sees the visible screen, so an answer longer than one screen is its tail.
  • Custom endpoints: tool calls through a BYOK endpoint were not exercised.
  • Remote SSH was not exercised against a real remote host.