Skip to content

Resolve Claude's home per worktree from the user's login shell - #326

Open
ljacomet wants to merge 2 commits into
ness-dev:mainfrom
ljacomet:fix-worktree-claude-config-dir
Open

ljacomet wants to merge 2 commits into
ness-dev:mainfrom
ljacomet:fix-worktree-claude-config-dir

Conversation

@ljacomet

@ljacomet ljacomet commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #325

Why

A folder-scoped CLAUDE_CONFIG_DIR is only visible to a login shell started in that directory. Main never asked one: chat tabs spawned the bundled binary from Ness's own env, and every transcript, resume and hooks lookup hardcoded ~/.claude.

What changes

  • New src/main/claude-env.ts. Runs the user's login shell in the worktree with a sentinel-wrapped env -0, caches the result per path, and re-probes on a spawn only when the entry is older than a minute so env edits made while Ness runs are picked up. Subdirectories resolve to their nearest cached ancestor. claudeConfigDirFor(cwd) falls back to Ness's env, then ~/.claude; an explicit claudeEnvVars.CLAUDE_CONFIG_DIR wins over the shell.
  • Chat tabs start from the captured env instead of process.env, so they see what a terminal tab's zsh -ilc sees.
  • Every ~/.claude/projects reader (--resume decision, transcripts, fork, context and cost trackers, cost roll-up) resolves through the same capture.
  • Hooks are installed into settings.json of every Claude home a worktree maps to, and a newly seen home gets hooked when consent is already accepted.
  • Hot path stays cold. A probe is a full shell startup, about a second on a heavy rc file. Sync readers only read the cache; the async entry points (startJsonClaudeSession, the agent:* request handlers, fork kickoff) await it, and worktrees are warmed on worktrees/listChanged. A chat start waiting on the probe absorbs a duplicate start and is cancelled by a kill, so closing a tab in that window leaves no orphan.
  • Stale header in JsonClaudeManager (per-session temp CLAUDE_CONFIG_DIR) replaced with what the code does; CLAUDE.md gets a quirk entry.
  • Probes are queued two at a time with a spawn's resolve jumping ahead
  • A failed probe is retried rather than cached

Testing

  • npm run typecheck, npx electron-vite build, npx vitest run all clean apart from three worktree-rename.integration tests that fail identically on main.
  • New claude-env.test.ts covers parsing, cwd, timeout and missing-cwd handling, ancestor lookup, override precedence, in-flight dedup, and keeping the last good capture over a failed refresh.
  • vitest.setup.ts clears CLAUDE_CONFIG_DIR so the suite does not depend on the developer's own Claude home.
  • Manual: through the built module, a worktree below the folder-scoped export resolves to the alternate home, a subdirectory to the same one, and /tmp to ~/.claude.

Caveat

Sessions whose transcript was written to ~/.claude before the folder-scoped export existed still cannot resume, since the new home has no copy of them.

A folder-scoped CLAUDE_CONFIG_DIR is only visible to a login shell
started in that directory, and main never asked one: chat tabs and every
transcript, hooks and resume lookup assumed ~/.claude.

Fixes ness-dev#325
Warming every worktree at once starts dozens of login shells together;
they all overrun the timeout and the failure was then cached as the
answer for the next minute, so a chat tab opened after a relaunch
resolved the wrong home and never resumed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude sessions ignore a folder-scoped CLAUDE_CONFIG_DIR: chat tabs, resume, status and cost all assume ~/.claude

1 participant