fix(tui): pass --safe-mode so the host CLAUDE.md cannot leak into proxied turns (#187)

OCP is a proxy: the operator's private ~/.claude/CLAUDE.md and auto-memory
must never enter a proxied turn's context. The pane already sets
CLAUDE_CODE_DISABLE_CLAUDE_MDS + CLAUDE_CODE_DISABLE_AUTO_MEMORY, but on
newer claude (2.1.216) those env vars no longer suppress the injection, so
a proxied turn can obey the operator's private CLAUDE.md instead of the
caller's prompt — a leak of private context into API responses.

Add --safe-mode to the interactive spawn argv (gated off on the streaming
and OCP_TUI_FULL_TOOLS paths). Docs + tests updated.

Co-authored-by: sumlin <3495838+sumlin@users.noreply.github.com>
This commit is contained in:
sumlin
2026-07-23 13:33:56 +10:00
committed by GitHub
co-authored by sumlin
parent c3294b404a
commit 8aac87aa61
3 changed files with 60 additions and 5 deletions
+32 -3
View File
@@ -389,9 +389,15 @@ export function buildTuiCmd(claudeBin, model, sessionId, ehome, entrypointMode,
// CLAUDE.md and auto-memory must NEVER leak into the agent OCP runs on the user's behalf.
// Without these, claude loads the host's project/user CLAUDE.md + memory into every proxied
// turn — verified live 2026-06-02: a cwd CLAUDE.md ("end every reply with QUACKMARKER_42") was
// obeyed by the proxied turn until these flags were set, after which it was not. Unconditional
// by design (not gated): proxy purity is not an opt-in. Harmless on hosts with no CLAUDE.md
// (the common case — they suppress nothing). Mirrors the -p path's CLAUDE_NO_CONTEXT vars.
// obeyed by the proxied turn until these flags were set, after which it was not. Mirrors the
// -p path's CLAUDE_NO_CONTEXT vars. Harmless on hosts with no CLAUDE.md (they suppress nothing).
//
// These env vars stopped being sufficient on newer claude: they are present in the pane's
// environment yet the host CLAUDE.md is still injected into the turn's context, so a proxied
// turn again obeys the operator's private CLAUDE.md instead of the caller's prompt — a leak of
// the operator's private context into API responses. The robust suppression is the --safe-mode
// flag added to the argv below; these env vars are kept as belt-and-braces for the two argv
// paths that cannot take --safe-mode (see the safeModeArgs note near the return).
const sets = [
`HOME=${shq(ehome)}`,
"DISABLE_AUTOUPDATER=1",
@@ -482,9 +488,32 @@ export function buildTuiCmd(claudeBin, model, sessionId, ehome, entrypointMode,
// so the OFF argv is byte-for-byte the pre-streaming argv.
const settingsArgs = stream && stream.settings ? ["--settings", shq(stream.settings)] : [];
// --safe-mode: disable ALL customizations (host CLAUDE.md, skills, plugins, hooks, MCP
// servers, custom commands/agents, output styles, ...) while leaving auth, model selection,
// built-in tools, and permissions untouched (claude 2.1.216 --help). This is the robust
// replacement for the CLAUDE_CODE_DISABLE_CLAUDE_MDS / _AUTO_MEMORY env vars above, which no
// longer suppress the host CLAUDE.md on newer claude — so the operator's private CLAUDE.md
// could otherwise leak into a proxied API response. Unlike --bare, --safe-mode does NOT drop
// the interactive session off the subscription pool (it keeps auth + model selection), so the
// billing classifier stays cc_entrypoint=cli.
//
// NOT applied when the pane deliberately relies on a customization --safe-mode would strip,
// because those would break SILENTLY:
// - streaming (settingsArgs present): the MessageDisplay hook is a HOOK, which --safe-mode
// disables — every streamed turn would then fire zero deltas (tui.streamZeroDeltaTurns)
// and fall back to buffered, defeating OCP_TUI_STREAM.
// - OCP_TUI_FULL_TOOLS=1: grants an MCP / skills / agent surface (--mcp-config, ...) that
// --safe-mode disables wholesale.
// Those two opt-in paths keep the env-var suppression above as their best-effort fallback.
const safeModeArgs =
settingsArgs.length === 0 && process.env.OCP_TUI_FULL_TOOLS !== "1"
? ["--safe-mode"]
: [];
return [
envPrefix,
shq(claudeBin),
...safeModeArgs,
"--model", shq(model),
"--session-id", sessionId,
...toolArgs,