Skip to content

Honor each agent's home relocation variable when resolving session dirs - #2375

Open
gtrrz-victor wants to merge 15 commits into
mainfrom
fix-hardcoded-claude-home
Open

Honor each agent's home relocation variable when resolving session dirs#2375
gtrrz-victor wants to merge 15 commits into
mainfrom
fix-hardcoded-claude-home

Conversation

@gtrrz-victor

@gtrrz-victor gtrrz-victor commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

entire session resume restored transcripts to ~/.claude/projects/... even when CLAUDE_CONFIG_DIR had relocated Claude Code's config directory, so Claude could not find the restored session (#2367). Every restore, attach fallback and transcript-owner lookup derives its directory from the agent's GetSessionDir, so the fix lives in the agent packages.

agent.ResolveHome is now the one place that reads an agent's relocation variable, and every agent's session-dir methods go through it.

Agent Variable Replaces
Claude Code CLAUDE_CONFIG_DIR ~/.claude (documented)
Gemini CLI GEMINI_CLI_HOME ~, .gemini still appended (documented)
Factory Droid FACTORY_HOME_OVERRIDE ~, .factory still appended (undocumented, read from the shipped binary)
Codex, Copilot CLI, pi CODEX_HOME, COPILOT_HOME, PI_CODING_AGENT_DIR already honored, now through the same resolver

Cursor gets no change: its CLI advertises a CURSOR_DATA_DIR transcripts folder, but the native writer stays under ~/.cursor (verified locally). A comment on its GetSessionDir records this so the bundle is not misread again.

Decisions worth a look

  • A relative value is refused, via userdirs.RequireAbsoluteOverride, rather than resolved against a cwd that differs between a hook and session resume. This now applies to Codex, Copilot and pi as well.
  • A blank value counts as unset; a non-blank value is used exactly as set, which is what the agents do.

Tests. An integration test reproduces the issue's flow and was red before the fix. Per agent, a resolver table test and one honors-test per session-dir method. A grep guard fails if any agent reads a relocation variable outside ResolveHome. Test harnesses scrub agent.RelocationEnvVars(). Not run: real-agent E2E.

One commit per agent, then one per review finding; the commit messages carry the detail.

Fixes #2367

🤖 Generated with Claude Code

gtrrz-victor and others added 2 commits September 11, 2026 12:24
`entire session resume` restored transcripts to ~/.claude/projects even
when CLAUDE_CONFIG_DIR relocated Claude Code's config directory, so Claude
could not find the restored session, and on setups that moved the config
dir to survive container rebuilds the restore landed on storage wiped by
the next rebuild while reporting success.

Claude Code documents that every ~/.claude path lives under
CLAUDE_CONFIG_DIR when it is set. One resolver now answers where Claude's
config dir is, and the session dir, session base dir, settings lookup and
review-skill discovery all go through it. A relative override is refused
rather than resolved against the working directory, which differs between
a hook and a command the user runs from a subdirectory.

Test harnesses unset CLAUDE_CONFIG_DIR so a developer's relocated config
dir cannot outrank the HOME steer the attach fallback tests rely on.

Fixes #2367

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M27ZYRPJK7DC21RCF4ZBSZ18
Gemini CLI resolves ~ through its own homedir() helper, which returns
GEMINI_CLI_HOME when set, and every storage path, chats included, appends
.gemini underneath it. Entire resolved the session dir and base dir from
the real home only, so `entire session resume` restored transcripts to
~/.gemini while Gemini looked under $GEMINI_CLI_HOME/.gemini, and attach's
cross-project fallback walked the wrong tree.

One resolver now answers which home Gemini uses, and both session-dir
methods go through it. The variable replaces the home directory, not
~/.gemini, so .gemini is still appended, unlike CLAUDE_CONFIG_DIR. A
relative override is refused rather than resolved against a per-process
working directory. Test harnesses unset GEMINI_CLI_HOME alongside
CLAUDE_CONFIG_DIR so a developer's relocated home cannot outrank the HOME
steer the attach fallback tests rely on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M280MCQZCQ05G2KY2S5J8350
Copilot AI lite review requested due to automatic review settings September 11, 2026 11:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Four unresolved moderate test-isolation findings affect Gemini, Claude resume, and Factory Droid integration tests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR makes session restoration and agent path discovery honor relocated directories for Claude, Gemini CLI, Cursor, and Factory Droid.

Changes:

  • Adds relocation-aware path resolvers.
  • Updates Claude settings and skill discovery.
  • Adds documentation and unit/integration test coverage.
File summaries
File Summary
docs/architecture/review-command.md Documents relocated Claude skill paths.
docs/architecture/agent-guide.md Documents agent relocation variables.
cmd/entire/cli/integration_test/setup_test.go Updates integration environment isolation. Open: scrub FACTORY_HOME_OVERRIDE.
cmd/entire/cli/integration_test/resume_test.go Tests Claude resume with relocation. Open: isolate child HOME and USERPROFILE.
cmd/entire/cli/global_test.go Clears relocation variables for CLI tests.
cmd/entire/cli/agent/geminicli/gemini.go Adds GEMINI_CLI_HOME resolution. Open: clear it in the default-path test.
cmd/entire/cli/agent/geminicli/gemini_test.go Tests Gemini path resolution. Open: clear GEMINI_CLI_HOME in the default-path test.
cmd/entire/cli/agent/factoryaidroid/factoryaidroid.go Adds Factory Droid home resolution.
cmd/entire/cli/agent/factoryaidroid/factoryaidroid_test.go Tests Droid path resolution.
cmd/entire/cli/agent/cursor/cursor.go Adds Cursor data-directory resolution.
cmd/entire/cli/agent/cursor/cursor_test.go Tests Cursor path resolution.
cmd/entire/cli/agent/claudecode/generate.go Uses Claude config resolution for settings.
cmd/entire/cli/agent/claudecode/discovery.go Discovers skills under relocated config.
cmd/entire/cli/agent/claudecode/discovery_test.go Tests relocated skill discovery.
cmd/entire/cli/agent/claudecode/claude.go Adds Claude config resolution for sessions.
cmd/entire/cli/agent/claudecode/claude_test.go Tests Claude session path resolution.
Review details

Suppressed comments (2)

cmd/entire/cli/agent/geminicli/gemini.go:126

  • This resolver now consumes GEMINI_CLI_HOME, but the existing TestGetSessionDir_DefaultPath only clears ENTIRE_TEST_GEMINI_PROJECT_DIR. A developer shell with a relative override will now make that test fail, while an absolute override makes it exercise the relocated path instead of the default. Please clear GEMINI_CLI_HOME in that default-path test (or in package test setup) so the default behavior remains hermetic.
	if dir := os.Getenv(geminiHomeEnvVar); dir != "" {
		if err := userdirs.RequireAbsoluteOverride(geminiHomeEnvVar, dir); err != nil {
			return "", err //nolint:wrapcheck // the error already names the override and its value
		}
		return dir, nil

cmd/entire/cli/integration_test/resume_test.go:1187

  • Because this test deliberately clears ENTIRE_TEST_CLAUDE_PROJECT_DIR, the subprocess falls back to its home directory. cmd.Env inherits the developer's real HOME, so if the resolver regresses or otherwise ignores CLAUDE_CONFIG_DIR, this test writes the restored transcript into the developer's ~/.claude and leaves it behind. Give the child a temporary HOME (and USERPROFILE on Windows) before exercising the fallback so the regression test remains hermetic.
	cmd.Env = append(testutil.GitIsolatedEnv(),
		"ENTIRE_TEST_CLAUDE_PROJECT_DIR=", // empty so the real resolution runs
		"CLAUDE_CONFIG_DIR="+configDir,
	)
  • Files reviewed: 16/16 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/entire/cli/agent/geminicli/gemini_test.go
Comment thread cmd/entire/cli/integration_test/setup_test.go Outdated
…ssion dirs

The droid binary resolves ~ through getFactoryHome(), which returns
FACTORY_HOME_OVERRIDE when set and os.homedir() otherwise, and its session
store appends .factory/sessions/<project> underneath. Entire resolved the
session dir and base dir from the real home only, so `entire session
resume` restored transcripts to ~/.factory while Droid looked under
$FACTORY_HOME_OVERRIDE/.factory, and attach's cross-project fallback
walked the wrong tree.

One resolver now answers which home Droid uses, and both session-dir
methods go through it. Like GEMINI_CLI_HOME the variable replaces the
home directory, not the dot-directory, so .factory is still appended. It
is undocumented; it is read from the shipped binary, where it is the only
relocation mechanism and the single input to getFactoryHome(). A relative
override is refused rather than resolved against a per-process working
directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M281REKSWZJV4A3B1W1R90E4
@gtrrz-victor
gtrrz-victor force-pushed the fix-hardcoded-claude-home branch from cf431ab to bcfb126 Compare September 11, 2026 11:23
gtrrz-victor and others added 5 commits September 11, 2026 13:55
Cursor keeps agent-transcripts under ~/.cursor even when CURSOR_DATA_DIR
relocates its other data, verified locally against cursor-agent
2026.09.08. Its CLI bundle does resolve a data dir from the variable and
advertises <data>/projects/<hash>/agent-transcripts to the model, which is
what a static read of the bundle finds; the files themselves are written
by the native file service, which stays on the real home. Following the
variable would point resume, attach and owner detection at a directory
Cursor never writes to. This comment exists so the bundle is not read
that way again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Six agents resolved their relocation variable with six copies of the same
dozen lines, and the copies had drifted into two policies: the three
added on this branch refused a relative value, Codex, Copilot and pi
accepted one, and nothing told a reader why CODEX_HOME=relative worked
while CLAUDE_CONFIG_DIR=relative did not. dupl never fires on a body this
short, so the next copy would have gone unflagged too.

agent.ResolveHome(envVar, defaultRel) is now the only implementation.
Every agent's resolver is a one-line call into it, so the policy is
uniform: a relative value is refused for all six, and a blank value counts
as unset again, which restores the TrimSpace the Claude settings lookup
had before this branch and matches how the agents read their own variable.
defaultRel records what each variable replaces, the dot-directory for
Claude, Codex, Copilot and pi, the home itself for Gemini and Droid.

The helper also refuses a variable missing from the static
relocationEnvVars list, exposed as agent.RelocationEnvVars for the test
harnesses. There is no interface to pin that list against, as there is
for CallerSessionEnvVars, so enforcement runs the other way: an agent
cannot start honoring a variable the harnesses do not scrub. The three
near-identical per-agent table tests collapse into one on the helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M285F858B54WKRYTRA0FWZ2D
The two TestMains unset CLAUDE_CONFIG_DIR and GEMINI_CLI_HOME by hand
under a comment that spoke of "the agents' homes" in general. Both lists
now come from agent.RelocationEnvVars, so a developer's FACTORY_HOME_OVERRIDE,
CODEX_HOME, COPILOT_HOME or PI_CODING_AGENT_DIR cannot outrank a test's HOME
steer either, and the comment describes what the code does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M285FB7MH47K0WFWJY2GJP26
…bles

Both the CLAUDE.md rule and the helper's own doc comment said it covered
"all three trees an override can redirect", which stopped being the whole
story once agent.ResolveHome started calling it for the agents' own
variables. Scope the three-trees claim to Entire's directories and say
why the agent case exists: nothing of Entire's is protected there, but a
relative value resolves against the repo root inside a hook and against
the user's cwd in a command, and Entire has to agree with where the agent
wrote.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M285FE19BTAT2GKGBKZAFXY3
AgentForTranscriptPath swallowed a GetSessionDir error with a bare
continue. That was invisible while the only error was a missing home
directory; now that a relative relocation variable is refused, a user with
one set has Claude Code silently stop owning any transcript path on every
hook, and the only symptom is a session attributed to the wrong agent.
Log the skip at debug with the agent and the error so .entire/logs names
the cause. The refusal itself stays: resume and attach already surface it
loudly, and resolving the value against a per-process cwd would scatter
transcripts instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M285FGK1WTRMPK60J1BGWNEH
@gtrrz-victor
gtrrz-victor force-pushed the fix-hardcoded-claude-home branch from bcfb126 to 56a42e3 Compare September 11, 2026 12:02
gtrrz-victor and others added 3 commits September 11, 2026 14:13
resolveCodexHome went through agent.ResolveHome, which refuses a relative
value and treats a blank one as unset, while codexConfigPath (trust.go)
and isUserHookRoot (hook_root.go) still read CODEX_HOME raw. One
environment therefore meant three things inside one agent: session dirs
refused it, trust inspection joined config.toml onto a cwd-relative path,
and the user-wide hook-root check canonicalized it against whatever the
process cwd was. Both sites now call resolveCodexHome and fail open the
way they already did for an unresolvable home: config path empty, trust
unknown; hook-root check false, root treated as the project's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2866B51ZX87ZQ3ZG962SMSC
The test cleared only the ENTIRE_TEST override, so with GEMINI_CLI_HOME
exported it either failed on a relative value or quietly stopped
exercising the home fallback on an absolute one. The cli TestMain scrub
does not reach this package. Clear the variable here too, and pin the
fallback layout instead of only asserting an absolute path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M287BEE0X8MCSRYQ6325CHG0
@gtrrz-victor

gtrrz-victor commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Copilot's summary listed two more items as suppressed comments without threads, both addressed:

  • integration_test/resume_test.go: the CLAUDE_CONFIG_DIR resume test now gives the child a throwaway HOME and USERPROFILE and asserts nothing lands under it, so a resolver regression cannot write into the developer's real ~/.claude. Fixed in 3b3c62f.
  • geminicli/gemini.go: duplicate of the Gemini default-path thread, fixed in fbe1e0f.

gtrrz-victor and others added 4 commits September 11, 2026 14:44
The test clears the ENTIRE_TEST override so the real resolver runs, and
inherited the developer's HOME. A regression that ignored
CLAUDE_CONFIG_DIR would therefore have restored the transcript into the
developer's own ~/.claude and left it there, which is what the first red
run of this test did. Hand the child a throwaway HOME and USERPROFILE and
assert nothing lands under it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M287XKCFTVEG0GPB9EYJ1RVY
ResolveHome trimmed the value it returned, and its comment claimed that is
how the agents read their own variable. They do not: Cursor tests
e?.trim() and then uses e, Claude reads process.env raw. So "/tmp/x "
resolved to /tmp/x for Entire and /tmp/x  for the agent, two directories
on POSIX, and Entire looked in the one the agent never wrote to. Trim for
the blank test only; return the raw value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M288S9NPE8CTT86WEJ1G3D5Z
isUserHookRoot and codexConfigPath fail open when resolveCodexHome errors,
which is right, but they did so silently. A refused CODEX_HOME then looked
like a project hook root, or like trust that happened to be unknown, and
nothing in .entire/logs said why. Log at debug, as AgentForTranscriptPath
already does for the same skip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M288SC4SAVS59GE9YG1DRWM9
ResolveHome refuses a variable missing from relocationEnvVars, which
catches an agent that calls it with a new name. It cannot catch an agent
that never calls it and reads os.Getenv("NEW_AGENT_HOME") inline, which is
how Copilot's COPILOT_HOME read looked before the shared resolver. Such a
read escapes the list the test harnesses scrub, and the symptom is a test
that passes on CI and fails on the one contributor with the variable set.

The guard greps every os.Getenv and os.LookupEnv under the agent tree and
allows exactly three shapes: the resolver itself, the caller-session
reader, and the ENTIRE_TEST_* overrides each agent keeps for its tests,
whether spelled as a literal or through a named constant. Verified to fail
on a planted inline read before committing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M288SEJ2CVES2NR2NMZ7MKSX
@gtrrz-victor
gtrrz-victor marked this pull request as ready for review September 11, 2026 13:00
@gtrrz-victor
gtrrz-victor requested a review from a team as a code owner September 11, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

session resume writes to ~/.claude instead of $CLAUDE_CONFIG_DIR

2 participants