Repository navigation
[CLAUDE ROUTINE]: CX/Reliability enhancement — extend scripts/preuninstall.mjs to clean up Codex / Copilot / Cursor hook entries so npm uninstall -g failproofai leaves zero orphans across every supported CLI #291
Description
Summary
failproofai already ships first-class install support for four agent CLIs on main — Claude Code, OpenAI Codex, GitHub Copilot CLI, and Cursor Agent — and failproofai policies --install --cli <name> happily wires up hook entries into each one's native settings file. The npm preuninstall lifecycle script that runs on npm uninstall -g failproofai, however, only knows how to clean up the Claude side (~/.claude/settings.json, <cwd>/.claude/settings.json, <cwd>/.claude/settings.local.json). The Codex / Copilot / Cursor entries it wrote at install time are left in place after uninstall, which means the next npm install -g failproofai (or a manual edit to those files) is the only way for users to get back to a clean slate.
This is an opportunity to bring uninstall parity in line with install parity — every CLI we register hooks for at install time should have its hooks gracefully unregistered at uninstall time, with the same "best-effort, never block" posture that the current Claude cleanup already follows.
Closely related but distinct from already-tracked items:
- bug: npm uninstall -g failproofai does not remove hooks from Claude settings #20 —
npm uninstall -g failproofai does not remove hooks from Claude settings. That issue is about the Claude side specifically; this issue is about extending the same hygiene to the other three (now four) CLIs that have shipped since bug: npm uninstall -g failproofai does not remove hooks from Claude settings #20 was filed in April. - [CLAUDE ROUTINE]: CX enhancement — make
postinstall.mjsaware of Codex / Copilot / Cursor so the "hooks not registered" warning only fires when something is actually missing #275 —make postinstall.mjs aware of Codex / Copilot / Cursor so the "hooks not registered" warning is correct. That issue is about the install-time warning; this is about the uninstall-time cleanup — the symmetric counterpart. - [CLAUDE ROUTINE]: CX/Reliability enhancement — add a confirmation step (or
--yes) tofailproofai policies --uninstall --scope allso a single typo can't sweep hooks out of every scope at once #268 —add a confirmation step (or --yes) to failproofai policies --uninstall --scope all. Different command path (subcommand-driven explicit uninstall vs. npm lifecycle).
Where it shows up today
scripts/preuninstall.mjs:30-88 — the cleanup function removeHooksFromFile() is hard-coded to Claude's matcher-wrapper schema (settings.hooks[eventType] → matchers[].hooks[]). The schema-walk logic itself only fits Claude:
for (const eventType of Object.keys(settings.hooks)) {
const matchers = settings.hooks[eventType]; // Claude shape
if (!Array.isArray(matchers)) continue;
for (let i = matchers.length - 1; i >= 0; i--) {
const matcher = matchers[i];
if (!matcher.hooks) continue; // Claude shape
matcher.hooks = matcher.hooks.filter((h) => {
if (h[FAILPROOFAI_HOOK_MARKER] === true) return false;
const cmd = typeof h.command === "string" ? h.command : "";
if (cmd.includes("failproofai") && cmd.includes("--hook")) return false;
return true;
});
// …
}
}scripts/preuninstall.mjs:96-101 — the candidate-paths list only enumerates Claude config locations:
const candidates = [
resolve(home, ".claude", "settings.json"), // user scope
resolve(projectCwd, ".claude", "settings.json"), // project scope
resolve(projectCwd, ".claude", "settings.local.json"), // local scope
];What's missing — every CLI that src/hooks/integrations.ts knows how to write to:
| CLI | User-scope path | Project-scope path |
|---|---|---|
| Claude | ~/.claude/settings.json |
<cwd>/.claude/settings.json + .local.json |
| Codex | ~/.codex/hooks.json |
<cwd>/.codex/hooks.json |
| Copilot | ~/.copilot/hooks/failproofai.json |
<cwd>/.github/hooks/failproofai.json |
| Cursor | ~/.cursor/hooks.json |
<cwd>/.cursor/hooks.json |
…and each of those uses a different schema, so a one-size-fits-all walker doesn't work either.
Coverage gap as it stands
flowchart LR
User[npm uninstall -g failproofai]
Pre[scripts/preuninstall.mjs]
User --> Pre
Pre -->|cleans| Claude[~/.claude/settings.json<br/>+ project + local]
Pre -.x.-> Codex[~/.codex/hooks.json<br/>+ project]
Pre -.x.-> Copilot[~/.copilot/hooks/failproofai.json<br/>+ .github/hooks/]
Pre -.x.-> Cursor[~/.cursor/hooks.json<br/>+ project]
style Claude fill:#d4edda,stroke:#28a745
style Codex fill:#f8d7da,stroke:#dc3545
style Copilot fill:#f8d7da,stroke:#dc3545
style Cursor fill:#f8d7da,stroke:#dc3545
Green = cleaned today. Red = orphaned hook entries left behind after uninstall.
Why it matters in user terms
- Reinstall reads stale config. A user who uninstalls and later reinstalls (or who installs a different version into a new global) will inherit those orphaned entries pointing at a binary path that may or may not still resolve. Because each CLI's hook command shells out to
npx -y failproofaior a pinned binary, the user may see "command not found" or, worse, a different version's behavior than they expect. - Hidden state across machines. Sysadmins and CI users who do
npm install -gon a build image and tear it down expect a clean tree. Today they have to know to alsorm -rf ~/.codex/hooks.jsonetc. — invisible to anyone who hasn't read this code. - Asymmetry surprises. Today a user can run
failproofai policies --install --cli codex --scope projectandnpm uninstall -g failproofaidoes the right thing for Claude but silently leaves Codex behind. The principle of least surprise says install/uninstall should mirror each other. - Beta CLIs lose trust. For users evaluating Codex / Copilot / Cursor support, "uninstall actually uninstalls" is a small but disproportionately confidence-building detail.
Lifecycle picture
sequenceDiagram
participant User
participant npm
participant Pre as preuninstall.mjs
participant Reg as integrations.ts registry
participant Claude as ~/.claude
participant Other as ~/.codex / ~/.cursor / ~/.copilot
Note over User,Other: TODAY
User->>npm: npm uninstall -g failproofai
npm->>Pre: spawn preuninstall
Pre->>Claude: removeHooksFromFile (3 paths)
Pre--xOther: (no awareness — orphans persist)
Pre->>npm: exit 0
npm->>User: "removed"
Note over User,Other: AFTER
User->>npm: npm uninstall -g failproofai
npm->>Pre: spawn preuninstall
Pre->>Reg: list integrations + per-CLI cleanup
Pre->>Claude: clean Claude (matcher-wrapper schema)
Pre->>Other: clean Codex / Copilot / Cursor (per-CLI schema)
Pre->>npm: exit 0 (totalRemoved sums across all CLIs)
npm->>User: "removed"
Proposed enhancement
Two viable shapes — pick whichever the team prefers:
Option A — inline per-CLI cleanup in preuninstall.mjs (zero new deps). Mirror the Node-builtins-only constraint of the existing script and add three small removeHooksFrom{Codex,Copilot,Cursor}File() walkers that match each CLI's actual schema. Reuse the same FAILPROOFAI_HOOK_MARKER + legacy-failproofai --hook fallback discipline already used for Claude. Extend the candidate-paths list to enumerate the user-scope and project-scope paths for each CLI from the table above.
Option B — call into the integrations registry. src/hooks/integrations.ts already exposes per-CLI removeHooksFromFile(settingsPath) methods on each Integration, and manager.ts removeHooks() already orchestrates the multi-CLI walk for failproofai policies --uninstall. The cleanest fix is to have preuninstall reuse that same orchestration. The catch: today preuninstall is intentionally Node-builtins-only so it works even if the package's own dist/ is broken. Option B would soften that guarantee — recommend Option A on safety grounds, with a small TODO to share parsing helpers if/when preuninstall is allowed to require the bundle.
Either way:
- Never block uninstall. Keep the existing
try { … } catch {}outer wrapper so a per-CLI failure (corrupt JSON, EACCES) doesn't strand the user mid-uninstall. - Per-CLI count in the summary log. The current
console.log("[failproofai] Removed N hook(s) from <path>")is great — extend it to print one line per CLI cleaned. - Telemetry parity. The existing
trackInstallEvent("package_uninstalled", { hooks_removed })already sums into onehooks_removedcounter. Either keep it as a sum (simpler) or split intohooks_removed_claude / _codex / _copilot / _cursor(more useful for diagnosing per-CLI install rates). - Extensible for [luv-271] feat: unified OpenCode + Pi CLI integrations (beta) #270 / [luv-277] feat: add Gemini CLI integration (beta) #277. When OpenCode / Pi / Gemini land on
mainthey'll plug into the same loop with one more table row — no code restructuring needed.
Acceptance criteria
- After
npm uninstall -g failproofai, zero failproofai-marked hook entries remain in~/.claude/settings.json,~/.codex/hooks.json,~/.copilot/hooks/failproofai.json,~/.cursor/hooks.json, or their project-scope (<cwd>/...) counterparts. - Third-party hooks in any of those files are preserved untouched.
- Corrupt / unreadable settings files for one CLI do not block cleanup of the others.
-
[failproofai] Removed N hook(s) from <path>lines print one per CLI per scope where work was done. - Unit tests in
__tests__/scripts/preuninstall.test.tsextend the existing Claude-only coverage with parallel cases per CLI: marked entry removed, legacy entry removed, third-party entry preserved, corrupt file → no-op, missing file → no-op. - Docker clean-install→uninstall test (per
CLAUDE.md§ Testing protocol) verifies an end-to-end uninstall against a session that had hooks installed for all four CLIs. -
CHANGELOG.mdentry under Unreleased → Fixes.
Severity
Medium — this is a CX / hygiene improvement, not a security or correctness defect. The visible symptom is "stale entries in CLI settings files after npm uninstall," which is mildly confusing for power users and mildly surprising for sysadmins. Bringing uninstall coverage in line with install coverage is a small, contained, high-confidence win that will only get more valuable as more CLIs are added.
Related
- bug: npm uninstall -g failproofai does not remove hooks from Claude settings #20 — Claude-side preuninstall cleanup (this issue extends the same hygiene to the other CLIs).
- [CLAUDE ROUTINE]: CX enhancement — make
postinstall.mjsaware of Codex / Copilot / Cursor so the "hooks not registered" warning only fires when something is actually missing #275 — postinstall awareness of Codex / Copilot / Cursor (symmetric install-side counterpart). - [CLAUDE ROUTINE]: CX/Reliability enhancement — add a confirmation step (or
--yes) tofailproofai policies --uninstall --scope allso a single typo can't sweep hooks out of every scope at once #268 — confirmation step onfailproofai policies --uninstall --scope all(different code path; same overall theme of safe uninstall UX). - [CLAUDE ROUTINE]: Reliability enhancement — atomic settings write to prevent concurrent-install clobber #150 — atomic settings write to prevent concurrent-install clobber (same files, install side).
Activity