Skip to content

fix(hooks): don't create root dirs for uninstalled tools on hooks inject/remove - #224

Merged
jeff-r2026 merged 1 commit into
mainfrom
fix/hooks-inject-uninstalled-tools
Jul 23, 2026
Merged

fix(hooks): don't create root dirs for uninstalled tools on hooks inject/remove#224
jeff-r2026 merged 1 commit into
mainfrom
fix/hooks-inject-uninstalled-tools

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Problem

teamai hooks inject / hooks remove were silently creating root directories for AI tools the user never installed.

The default toolPaths includes internal agents like tclaude, tcodex, codex-internal. When a user ran hooks inject, empty ~/.tclaude/, ~/.tcodex/, ~/.codex-internal/ directories appeared out of nowhere — each with a settings.json/hooks.json and empty skills/rules/agents/ subdirs. Worse: once those root dirs existed, isToolInstalled treated the tools as installed, so subsequent teamai pulls copied skills into them too.

Root cause

hooks inject/remove passed force: true to reconcileHooksToAllTools (introduced in #135 to make clean-env E2E tests pass). force skipped the pathExists(toolRoot) gate, and reconcileHooks then created the root dir via ensureDir for every configured tool.

// before — force bypassed the installed check entirely
if (!opts.force) {
  const toolRoot = path.join(baseDir, paths.settings.split('/')[0]);
  if (!await pathExists(toolRoot)) continue;
}

force's only effect was bypassing this gate, so it's removed entirely. Hooks are now reconciled only into tools whose root directory already exists — consistent with skill/rule/agent sync and with injectHooksToAllTools (the pull/init path, which already gated correctly).

Changes

  • src/hooks.ts: drop the force option; always gate each tool on root-dir existence
  • src/hooks-cmd.ts: stop passing force: true from inject/remove
  • Tests: pre-create installed tool dirs where they previously relied on force; add an e2e regression asserting uninstalled tool dirs are never created
  • Docs: clarify inject/remove only touch installed tools (README + usage-guide, both languages)

Test Plan

  • npx tsc --noEmit — clean
  • npx vitest run — 1836 passed
  • npx vitest run --config vitest.e2e.config.ts — 56 passed, 23 skipped
  • New regression test does not create root directories for uninstalled tools (hooks-team-e2e) passes
  • Real CLI e2e in a clean $HOME: only .claude (pre-installed) exists after hooks inject; .tclaude / .tcodex / .codex-internal / .cursor / .codex / .codebuddy are not created, and .claude/settings.json is written correctly

🤖 Generated with Claude Code

…ect/remove

`teamai hooks inject` / `hooks remove` passed `force: true` to
`reconcileHooksToAllTools`, which skipped the "is the tool installed?"
gate. `reconcileHooks` then created the tool's root directory via
`ensureDir` for *every* tool in `toolPaths` — including ones the user
never installed (`.tclaude`, `.tcodex`, `.codex-internal`, all present
in default toolPaths).

Once those root dirs existed, `isToolInstalled` treated the tools as
installed, so subsequent `pull`s copied skills into them — surfacing as
mysterious `.tclaude/skills/...` directories the user never asked for.

Fix: always apply the tool-installed gate. `force` only ever bypassed
this check, so the option is removed entirely. Hooks are now reconciled
only into tools whose root directory already exists — matching the
behavior of skill/rule/agent sync and `injectHooksToAllTools`.

- src/hooks.ts: drop `force`, gate every tool on root-dir existence
- src/hooks-cmd.ts: stop passing `force: true` from inject/remove
- tests: pre-create installed tool dirs where they relied on `force`,
  add an e2e regression asserting uninstalled tool dirs are never created
- docs: clarify inject/remove only touch installed tools (README + guide, both langs)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeff-r2026
jeff-r2026 merged commit 136ef9a into main Jul 23, 2026
7 checks passed
@jeff-r2026
jeff-r2026 deleted the fix/hooks-inject-uninstalled-tools branch July 23, 2026 04:50
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.

1 participant