Skip to content

fix(externagent): run claude and codex against their own homes, never copy their tokens - #643

Merged
ion-alpha-dev merged 6 commits into
mainfrom
fix/claude-own-config-home
Nov 1, 2026
Merged

ion-alpha-dev merged 6 commits into
mainfrom
fix/claude-own-config-home

Conversation

@ion-alpha-dev

@ion-alpha-dev ion-alpha-dev commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

What

Flynn now keeps a separate home for each external CLI under its data directory (claude/ and codex/) and runs the CLI against it, instead of copying the user's own credential (~/.claude/.credentials.json, or auth.json from ~/.codex or CODEX_HOME) into a per-run home for every episode. Flynn never reads, copies or stores the token inside.

Why

The user's personal sign-in belongs to their own use of the CLI, and Flynn had no reason to handle it. A copied token also lives outside the CLI's control: the CLI cannot refresh the original, and the copy sits in a directory the user never asked for. With a home of its own, each CLI signs in through its own login flow, keeps its token where it put it, and refreshes it in place.

How to verify

  • go test ./cmd/flynn ./externagent
  • TestDedicatedAuthDirIsWrittenInPlace: a home marked writable (SandboxConfig.AuthDirWritable) is used in place with nothing copied, and an unmarked home stays read-only.
  • TestOwnHomeSpawnersGrantWriteOnTheirHome: the claude and codex envelopes set AuthDirWritable, AuthDir and the right environment variable.
  • TestClaudeHomeIsFlynnsOwnAndNeverTheUsers, TestCodexHomeIsFlynnsOwnAndNeverTheUsers: each backend uses Flynn's home whatever the user's environment variable says, and the home exists before detection. The codex test also checks that config.toml, AGENTS.md and a skills directory are removed while auth.json is kept.
  • TestExternalHomesAreOwnerOnly: an existing home with mode 0755 is 0700 once the spawner is built (the mode assertion is skipped on Windows, where permission bits do not express owner-only access).
  • TestClaudeHomeKeepsOnlyItsSignIn: settings.json, CLAUDE.md, .claude.json and an agents directory are removed from the claude home and .credentials.json is kept.
  • TestClearOwnHomeReportsAnUnreadableHome, TestClearOwnHomeReportsAnUnremovableEntry, TestExternalSpawnerRefusesAHomeItCannotPrepare, TestEnsureOwnHomeReportsAFailure: the non-ENOENT error paths.
  • TestLoginSignsInFlynnsHome: the sign-in message for both backends names the right variable and login command, quotes the path for sh and PowerShell, and never asks for an API key.
  • By hand: run flynn --model codex (or claude) with no sign-in; it prints a CODEX_HOME=... codex login (or CLAUDE_CONFIG_DIR=... claude auth login --claudeai) command. After running it, the backend reports itself ready and ~/.codex / ~/.claude are unchanged.

Notes for reviewers

  • Anyone already using --model claude or --model codex signs in once more. Their own ~/.claude and ~/.codex sign-ins are untouched.
  • The user's own settings, hooks, MCP servers and config.toml are never on the child's path. The child may write Flynn's codex home, so before a spawner is built everything in that home except auth.json is removed (config, AGENTS.md, prompts, skills, history, caches); the CLI recreates what it needs. The claude home gets the same treatment through the shared clearOwnHome, keeping only .credentials.json: settings.json (hooks, permission allow rules), CLAUDE.md, agents/, commands/, plugins/ and .claude.json are removed. With CLAUDE_CONFIG_DIR set, Claude Code keeps .claude.json in that directory, and it holds user-scoped MCP servers and trust decisions, so it cannot be kept; the account details in it are re-read from the token. I did not run the claude CLI against a cleared home to confirm its status check still reports signed in, so that is worth confirming by hand. A sign-in held in the OS keyring is outside the home and untouched. MkdirAll leaves an existing directory's mode alone, so both homes are chmod to 0700 explicitly.
  • The PowerShell branch of homeLoginCommand has no Linux CI coverage because it is selected by runtime.GOOS == "windows", and codecov uploads the Linux coverage profile. The Windows test job does run TestLoginSignsInFlynnsHome, which asserts the quoted path and the trailing Remove-Item. The branch is two lines of string concatenation, and no Linux test can reach it without making the OS a parameter.
  • Uncovered lines expected in the Linux patch report: the os.Chmod failure return in ensureOwnHome. It can only fail after MkdirAll succeeded on the same path (a race with another process or a filesystem that refuses chmod), so there is no portable way to provoke it. The RemoveAll failure return in clearOwnHome and both spawners' clear failures are covered on Linux and skipped on Windows and as root, because directory permissions do not block removal there.

@ion-alpha-dev ion-alpha-dev changed the title fix(externagent): run claude against its own home, never copy its token fix(externagent): run claude and codex against their own homes, never copy their tokens Oct 31, 2026
@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cmd/flynn/externagent.go 95.16% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ion-alpha-dev

Copy link
Copy Markdown
Collaborator Author

Review: the change is right, and no token copy path is left. Four things to fix before merge:

  1. MkdirAll(0o700) does not tighten a directory that already exists. If <dataDir>/claude or <dataDir>/codex was created earlier with a wider mode, it keeps that mode. Chmod after creating, and add a test that asserts 0700.
  2. The child can write the codex home (AuthDirWritable: true), but clearCodexConfig removes only config.toml. A run could leave behind AGENTS.md, prompts or skills that steer the next run. Either clear everything except the auth files, or document the remaining risk beside clearCodexConfig.
  3. No test asserts that AuthDirWritable: true is set in the claude and codex spawner configs. Add one so a regression fails CI.
  4. The non-ENOENT error return in clearCodexConfig needs a test (make config.toml a non-empty directory). The PowerShell branch of homeLoginCommand runs only on Windows, so a sentence about it in the PR body covers it.

go test -race ./externagent/... ./cmd/flynn/... passes locally.

The claude backend used to read the user's Claude Code credential file
and config out of ~/.claude and copy them into a per-run directory for
each episode. Flynn handled the OAuth token itself, and a token the CLI
refreshed mid-episode was refreshed only in the copy.

The backend now keeps a Claude Code home of its own under the data
directory. The user signs the CLI in to it once with the CLI's own
login, and every probe and episode points the CLI at it through
CLAUDE_CONFIG_DIR. Flynn never reads, copies or deletes the token; the
CLI writes it at sign-in and refreshes it in place. The user's own home,
settings, hooks and MCP servers are no longer read at all.

SandboxConfig gains AuthDirWritable so an episode may write such a
dedicated home. A user's own CLI home stays read-only to the child, as
before. When claude is installed but not signed in, the onboarding
message now prints the sign-in command for Flynn's home, quoted for
PowerShell on Windows and for sh elsewhere.

Signed-off-by: Ion Alpha <[email protected]>
The codex backend copied auth.json out of the user's own codex home
into a per-run home for every episode. It now gets the same treatment
as claude: Flynn keeps a codex home of its own under its data
directory, the user signs the CLI in to it once with `codex login`
(Flynn prints the exact command), and probes and episodes point the
CLI at it through CODEX_HOME with write access, so the CLI refreshes
its token in place and Flynn never reads it.

The user's config.toml is never on the child's path. A config.toml
found in Flynn's own codex home is removed before a spawner is built,
so a harness cannot leave one behind to steer a later run.

The not-signed-in message for both backends now comes from one
helper, and the login command is built by one function per shell.

Existing codex users sign in once more; their own ~/.codex is
untouched.

Signed-off-by: Ion Alpha <[email protected]>
MkdirAll leaves an existing home with the mode it already had, and the
home holds a bearer token, so the claude and codex homes are now chmod
to 0700 when the spawner is built.

An episode's child can write the codex home, so removing only
config.toml left AGENTS.md, prompts and skills free to steer the next
run. The home is now cleared of everything except auth.json, the
sign-in the CLI keeps in its file credential store.

The claude and codex sandbox configs are built by named functions so a
test can hold AuthDirWritable to true, and the error paths of the home
preparation are tested.

Signed-off-by: Ion Alpha <[email protected]>
The claude home is writable by the episode's child, and nothing cleared
it between runs, so settings.json hooks, CLAUDE.md, agents, commands or
plugins left by one episode would steer the next. clearCodexHome is
generalised to clearOwnHome over a keep-set, and the claude branch of
externalSpawner now calls it keeping only .credentials.json.

.claude.json is cleared too: with CLAUDE_CONFIG_DIR set it lives in the
home and holds user-scoped MCP servers and trust decisions. The token
is in .credentials.json, so the CLI only re-reads the account details.

The claudeHome comment no longer says the worst a harness can do is
sign the copy out.

Signed-off-by: Ion Alpha <[email protected]>
The spawner test for a home that cannot be cleared now runs against
both backends, so the claude branch's error return is exercised.

Signed-off-by: Ion Alpha <[email protected]>
@ion-alpha-dev
ion-alpha-dev force-pushed the fix/claude-own-config-home branch from f1ded8b to a66691a Compare November 1, 2026 09:20
@ion-alpha-dev

Copy link
Copy Markdown
Collaborator Author

Second review: the codex fixes are good. The claude home still has the problem that was fixed for codex. claudeSandboxConfig grants the episode write on <dataDir>/claude (AuthDirWritable: true), but nothing clears that home between runs. Claude Code reads settings.json (hooks, permission allow lists), CLAUDE.md (user memory), agents/, commands/ and plugins/ from CLAUDE_CONFIG_DIR. A harness could write any of them and steer every later claude run. --strict-mcp-config neutralizes only the MCP servers. The claudeHome doc comment says "the worst a harness can do there is sign Flynn's copy out", which is not true while that holds.

Please clear the claude home the same way: keep only the sign-in (.credentials.json, plus .claude.json if the CLI reports itself unusable without its onboarding and account state) and remove everything else before the spawner is built. Share the code with clearCodexHome (one function over a keep-set), test it with settings.json, CLAUDE.md and an agents directory, and correct the comment.

@ion-alpha-dev
ion-alpha-dev merged commit 1e1b151 into main Nov 1, 2026
32 checks passed
@ion-alpha-dev
ion-alpha-dev deleted the fix/claude-own-config-home branch November 1, 2026 10:02
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant