fix: stop apply pin escaping to $HOME via the global ~/.claude#19
Merged
Conversation
findClaudeAncestor checked dir/.claude before the dir===home boundary, so on a fresh clone (repo .claude/ is gitignored, hence absent) the pin walk (stopAtGit:false) climbed to $HOME, matched the global ~/.claude config dir, and wrote .umbel-bundle at $HOME — silently routing every home project's plain claude through one bundle. Treat home as an exclusive upper boundary (checked before the .claude match), matching the function's own documented contract. pinPath then falls back to cwd (the repo root). Also closes the same latent escape in skills/bundle discovery for non-git dirs under $HOME. Closes #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
umbel apply <bundle>wrote.umbel-bundleto$HOMEinstead of the repo root when the repo had no.claude/directory — silently routing every home project's plainclaude(via the PATH shim) through one bundle. Closes #14.Root cause
findClaudeAncestor(src/target/walk.ts) checkeddir/.claudebefore thedir === homeboundary.~/.claudeis the global Claude config dir — present on every machine that has run Claude, never a project marker. On a fresh clone (the repo's.claude/is gitignored, hence absent), the pin walk (stopAtGit: false) climbed past the repo, reached$HOME, matched~/.claude, and returned$HOME→ pin written at$HOME/.umbel-bundle. The function's own doc already promised "returns null … before reaching home" — the code violated its own contract.Fix
One reorder: check
dir === homebefore the.claudematch, makinghomean exclusive upper boundary.pinPaththen falls back tocwd(the repo root in the reported reproduction).Fixed at the shared
findClaudeAncestorrather than patched at the pin layer, so the same latent$HOME-escape is also closed for skills/bundle discovery (non-git dirs under$HOME). Blast radius across all four consumers (findProjectRoot,findClaudeSkillsDir,findClaudeBundlesDir, bundle-init) verified benign — the only behavior shift is no longer treating the global~/.claudeas a project root, which is exactly what the issue asks for. Applying at$HOME(cwd === home) still writes a home pin via the cwd fallback; only upward escape from a subdir is prevented.Tests (TDD — failed first, then fixed)
pin.test.ts: pin anchors at the repo (not$HOME) when the repo lacks.claude/;findProjectRootstops at the home boundary;cwd === homefalls back to cwd.target.walk.test.ts:homeis an exclusive boundary even when~/.claudeexists.Verification
npm test→ 341 passingtsc --noEmitclean ·biome check .clean