Repository navigation
fix(toolRoots): accept roots nested below ~/.config/<name> - #1017
jeff-r2026 merged 1 commit into
Conversation
|
The earlier stale-root finding is resolved by the exact-root gate and its propagated call sites. The PR description includes a representative real-CLI verification, so its testing record is sufficient. |
0a4b519 to
a36cfa1
Compare
The earlier stale-root runtime finding is resolved by the exact-root gate and its propagated call sites. The PR description includes a representative real-CLI verification, so the testing record is sufficient. |
The gate derived the root from the path's shape, so a root nested below ~/.config/<name> was probed at its two-segment prefix (.config/cloak) — a directory that may exist for an unrelated reason while the recorded root itself does not. That let the gate call the tool installed and the write recreate a root the user had deleted. Split the two meanings: toolInstallRoot keeps answering from the path shape (which is what relocation keys on), toolGateRoot answers with the recorded root when the path really hangs off it and falls back to the shape otherwise, and isAddressableRootSegment accepts any depth under .config/. The docs and the agent-facing skill-data move with it: the guide (both languages) and troubleshooting no longer say the check reads .config/<name>, and the root-rejection message drops the same stale justification — .config/ is required so the check can tell the tool's root from an unrelated directory, not because that prefix is what it reads.
a36cfa1 to
3a23aa4
Compare
|
No findings.
|
CI note: the one red check is a pre-existing flake, not from this PR
The single error is an unhandled i.e. a TOCTOU in Two cross-checks that it is unrelated to this change:
So this PR is |
Resolve the conflict in src/resources/base.ts: upstream Tencent#1017 added toolGateRoot to the installed-tool gate. The Antigravity probe stays ahead of the generic call — Antigravity is not in RELOCATABLE_TOOLS, so a recorded toolRoots entry does not apply to it - and the generic call now uses upstream's toolGateRoot form. Switch the Antigravity hooks install gate in hooks.ts to the same toolGateRoot shape upstream adopted, which is what upstream's import list expects (toolInstallRoot is no longer imported there).
Closes #1010.
CLAUDE_CONFIG_DIRcan point several levels below~/.config: a per-directory profile manager such as cloak puts it at~/.config/cloak/profiles/work/claude.teamai initrefused to record that root, warned, and left the sync on~/.claude— so the hooks were written to a settings file the tool never reads, and the session sync silently did nothing.Why this is more than relaxing one predicate
My first revision only loosened
isAddressableRootSegment. That is not enough, and looking at it the way the gate does shows why: the installed-tool gate does not read the recorded root — it derives one from the path's shape.toolInstallRootkeys a.config/...path on its first two segments, so for the root above it probes.config/cloak. That directory can exist for an unrelated reason while the recorded root itself does not, and then the gate calls the tool installed and the write recreates a root the user had deleted — the very thing the gate exists to prevent.So the two meanings get separated.
What changed
toolInstallRootkeeps answering from the path's shape.relocateToolPathsmatches against it andapplyToolRootsre-roots at it, so it must stay independent of any recorded root.toolGateRootis new, and is what the gate reads:The record is used only when
toolPathreally hangs off it — the sametoolPathsare used unrelocated in project scope, where a HOME-relative member root says nothing — and only aftertoolRootSegmenthas vetted it (unregistered tool, rejected root: both fall through). With no recorded root the two functions agree, so every existing setup behaves exactly as before.isAddressableRootSegmentthen accepts any depth under.config/:Roots nested outside
.config(~/configs/claude,~/.local/share/claude) stay refused.Gate call sites now thread the record through:
injectHooksToAllTools,reconcileHooksToAllTools,hooksRemove, the two user-scope probes inpull.tsandpre-push-sync.ts, andknown-agents.ts'sseedToolRoots/seedHookInstallRoot— the last two have to agree with the gate, or a self-mode nested root would never get its hooks.The text that describes the root moves with the code.
docs/guide/reference.mdand its Chinese pair no longer say the check keys on.config/<name>; it reads the recorded root.skill-data/core/references/troubleshooting.mdno longer tells agents a root nested below~/.config/<name>cannot be synced. AndtoolRootRejectiondrops the same stale justification —.config/is required so the check can tell the tool's root from an unrelated directory, not because that prefix is what it reads.Verified
Real CLI, single-variable pair —
teamai hooks injectwithCLAUDE_CONFIG_DIR=$HOME/.config/cloak/profiles/work/claude. The only thing that varies is whether the recorded root exists on disk:toolGateRootdegrades totoolInstallRoot).config/cloakexists✔ Updated teamai hooks in …\.config\cloak\profiles\work\claude\settings.json, and the whole stale path is created✔ Hooks injected into all AI tool settings; under.configthe tree stays.config,.config/cloak— nothing writtenThe control build reproduces the failure mode this PR is about; this branch stops it while still delivering to a root that is really there.
Unit tests —
src/__tests__/tool-roots.test.ts, 42 passed, including six cases fortoolGateRoot(reads the record; agrees with the path shape whenever the two can agree; ignores a record the path does not hang off; ignores a record the tool may not relocate; still skips a missing two-segment root) and a single-variable pair onisToolInstalledForConfig.Mutation-checked: dropping the record branch inside
toolGateRootturns exactly the four intended cases red (4 failed / 38 passed) and leaves the controls green.tsc --noEmitandoxlintare clean.Regression check
The branch is rebased onto
2cbf5560: the tree is that commit plus exactly these thirteen files. #1002 (#904) touched the same gate function — the tail ofisToolInstalledForConfiginsrc/resources/base.ts— so the resolution keeps itsEDITION_HOME_ROOTSbranch first and threadstoolGateRootinto the probe call after it;src/types.tskeeps #1002's Trae schema andMCP_MANIFEST_KEY_ALIASalongsidetoolGateRoot.Against a pristine
2cbf5560checkout, the eight gate-related test files produce identical failure sets —tool-roots,doctor,trae,qoder-cn,hooks,hooks-cmd,init,instruction-targets; every failure is already on the base (Windows path shapes,EBUSYunder the runner, the threefilterAgentscases).Against the original base
b69f8889the affected test files behave the same: eleven identical failure sets, andpull-agent-modelsproduces one fewer failure than the base (11 vs 12) — onlyTest timed out in 15000ms/EBUSY: resource busy or locked, rmdir …\Temp\…, no assertion on either revision.Not touched
applyToolRoots/relocateToolPaths/resolveToolRootDirare string operations and were already depth-agnostic. The.config/<name>keying intoolInstallRootis deliberately unchanged.toolRootsfor tools other thanclaude/codexis unchanged.