You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
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:
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(consteventTypeofObject.keys(settings.hooks)){constmatchers=settings.hooks[eventType];// Claude shapeif(!Array.isArray(matchers))continue;for(leti=matchers.length-1;i>=0;i--){constmatcher=matchers[i];if(!matcher.hooks)continue;// Claude shapematcher.hooks=matcher.hooks.filter((h)=>{if(h[FAILPROOFAI_HOOK_MARKER]===true)returnfalse;constcmd=typeofh.command==="string" ? h.command : "";if(cmd.includes("failproofai")&&cmd.includes("--hook"))returnfalse;returntrue;});// …}}
constcandidates=[resolve(home,".claude","settings.json"),// user scoperesolve(projectCwd,".claude","settings.json"),// project scoperesolve(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
Loading
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 failproofai or 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 -g on a build image and tear it down expect a clean tree. Today they have to know to also rm -rf ~/.codex/hooks.json etc. — invisible to anyone who hasn't read this code.
Asymmetry surprises. Today a user can run failproofai policies --install --cli codex --scope project and npm uninstall -g failproofai does 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"
Loading
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 one hooks_removed counter. Either keep it as a sum (simpler) or split into hooks_removed_claude / _codex / _copilot / _cursor (more useful for diagnosing per-CLI install rates).
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.ts extend 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.md entry 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.
Summary
failproofaialready ships first-class install support for four agent CLIs onmain— Claude Code, OpenAI Codex, GitHub Copilot CLI, and Cursor Agent — andfailproofai policies --install --cli <name>happily wires up hook entries into each one's native settings file. The npmpreuninstalllifecycle script that runs onnpm 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 nextnpm 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:
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.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.--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 functionremoveHooksFromFile()is hard-coded to Claude's matcher-wrapper schema (settings.hooks[eventType] → matchers[].hooks[]). The schema-walk logic itself only fits Claude:scripts/preuninstall.mjs:96-101— the candidate-paths list only enumerates Claude config locations:What's missing — every CLI that
src/hooks/integrations.tsknows how to write to:~/.claude/settings.json<cwd>/.claude/settings.json+.local.json~/.codex/hooks.json<cwd>/.codex/hooks.json~/.copilot/hooks/failproofai.json<cwd>/.github/hooks/failproofai.json~/.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:#dc3545Green = cleaned today. Red = orphaned hook entries left behind after uninstall.
Why it matters in user terms
npx -y failproofaior a pinned binary, the user may see "command not found" or, worse, a different version's behavior than they expect.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.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.Lifecycle picture
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 smallremoveHooksFrom{Codex,Copilot,Cursor}File()walkers that match each CLI's actual schema. Reuse the sameFAILPROOFAI_HOOK_MARKER+ legacy-failproofai --hookfallback 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.tsalready exposes per-CLIremoveHooksFromFile(settingsPath)methods on eachIntegration, andmanager.ts removeHooks()already orchestrates the multi-CLI walk forfailproofai 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 owndist/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:
try { … } catch {}outer wrapper so a per-CLI failure (corrupt JSON, EACCES) doesn't strand the user mid-uninstall.console.log("[failproofai] Removed N hook(s) from <path>")is great — extend it to print one line per CLI cleaned.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).mainthey'll plug into the same loop with one more table row — no code restructuring needed.Acceptance criteria
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.[failproofai] Removed N hook(s) from <path>lines print one per CLI per scope where work was done.__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.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
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).--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).