Skip to content

fix(uninstall): remove the machine-wide hooks when nothing else uses them - #1826

Closed
Walternater wants to merge 0 commit into
Tencent:mainfrom
Walternater:fix/last-install-hook-removal
Closed

Walternater wants to merge 0 commit into
Tencent:mainfrom
Walternater:fix/last-install-hook-removal

Conversation

@Walternater

@Walternater Walternater commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

teamai uninstall in a project now removes the machine-wide hook files it wrote when no other teamai install on the machine runs them. It used to keep Codex's ~/.codex/hooks.json, OpenCode's plugin, and the Pi, OMP, Kiro and Antigravity hook files for "other teamai installs (user scope, HTTP agent or other projects)" without checking that one existed — so on the last install those dispatchers stayed, fired on every session and found no config, and teamai hooks remove was the only way out.

The decision is the one #1817 added for the wrapper: installsSharingHome(dataHome) for the user scope and the other project partitions, plus ~/.teamai/local-agent/manifest.json for an HTTP agent. When any of them is set up, nothing changes: the files are kept and the message still names them.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes
  • npx vitest run src/__tests__/uninstall.test.ts src/__tests__/mcp-uninstall.test.ts passes
  • Added tests for the change

Unit

Three cases, in uninstall.test.ts beside the user-scope one:

case fixtures expectation
last install project scope, dataHome set, no user config, no other partition ~/.codex/hooks.json is gone, and so is the data home the uninstall ran in
another install the same, plus a second partition with a config and an anchor the file stays, and the summary still says run teamai hooks remove here first
user scope unchanged, scope: user the file is gone (as before)

The data-home assertion is there because CI's Agent wait budget caught the first version keeping it: keptForProjects() was gated on globalAdapters, so a project uninstall that took the machine-wide adapters also asked which projects were still set up and kept its own. That gate is on the scope again.

Negative control on the same head: with globalAdapters = scope === 'user' put back, the first case fails —

× removes the global Codex hooks when this is the last install on the machine
AssertionError: expected true to be false

— and the second still passes, which is what "kept while another install exists is unchanged" means.

Real CLI, after npm run build

Verify world, workspace scope, one agent, workspace init + pull, then uninstall --force:

$ npm run verify -- world                        # 20261111T023018-27256d
before: present                                  # ~/.codex/hooks.json, all six event lists
$ npm run verify -- teamai <world> uninstall --force
✔ teamai uninstalled
after: gone

The kept side, same commands, with a second project's data home planted before the uninstall:

$ npm run verify -- world                        # 20261111T023038-c6ab60
before: present
ℹ  Kept for other teamai installs on this machine (user scope, HTTP agent or other projects):
     /…/home/.codex/hooks.json
   If none of them uses these, cancel and run `teamai hooks remove` here first: it removes them.
after: present

The same commands on main before this change are #1825's table: the file and all of its dispatchers were still there with nothing else installed.

Related Issues

Fixes #1825.

Notes for Reviewers

  • One boolean, six adapters. globalAdapters is what every adapter whose hooks live in a user-wide file already consults, so this adds a case to it rather than a branch per tool: scope === 'user' || await lastInstallOnMachine(localConfig.dataHome). No adapter's own code changed.
  • What counts as "another install". The user scope (loadLocalConfig), every other partition under ~/.teamai/projects but this one, and an HTTP local agent. A partition whose config cannot be read counts as another install, so the failure mode is keeping, not removing.
  • Unknown data home keeps them. dataHome is set on a project config; without it the helper answers false and the old rule stands, which is why the existing "keeps global delivery" cases in this file still pass untouched.
  • Remainder, not regressions. Codex's hooks.state approvals for a removed hooks.json stay in config.toml; teamai hooks remove leaves them today as well, so this change does not introduce it. The empty tool directories (~/.codex, <project>/.codex) stay on purpose — fix(uninstall): remove what teamai created and left empty #1817 established that a tool's own directory is not ours to delete.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] src/uninstall.ts:988 — The last-install check is not conservative when another partition’s config cannot be read. installsSharingHome explicitly skips unreadable configs, so this check can authorize deleting shared hooks despite that partition still existing. After its config is repaired, the other installation has lost its hooks. Preserve them when another partition has an unreadable config, as the PR description promises.

  • [P2 non-blocking] src/dashboard-collector.ts:460 — The host-blocked tool-result filtering, accompanying tests, marker, and changelog entry address Friction reminder counts Claude Code auto-mode classifier timeouts as failing tools #1821, not this PR’s uninstall goal. This violates the review rule requiring surgical changes tied to the stated goal; move those changes to a separate PR.

The description includes sufficient representative real-CLI verification for uninstall, covering both removal and preservation. Nothing from the PR was run, built, or installed.

@Walternater
Walternater force-pushed the fix/last-install-hook-removal branch from b314494 to b27e94c Compare November 11, 2026 02:42
@Walternater

Copy link
Copy Markdown
Collaborator Author

The Agent wait budget job caught a regression in this change, and it was real.

keptForProjects() was gated on globalAdapters, which until this change meant scope === 'user'. Making it true for a project uninstall on the last install also made that uninstall ask "which projects are still set up?" — and its own project answers yes, because the env loader still routes shells to it. So the data home the uninstall was running in got kept, and the ledger reported:

LEAK home/.teamai/projects/<slug> left behind, with everything in it

The gate is on the scope again: a user-scope removal leaves the other projects' data; a project removal on the last install has no other project to leave. globalAdapters now only decides the machine-wide adapters, which is what this PR is about.

The new case asserts the data home is gone as well — the first version only looked at the hook file, which is why it passed while the ledger failed.

Locally, on this head: node scripts/agent-wait-budget.mjs reported 2 LEAKS before the fix (the partition plus project/.git/packed-refs) and 1 LEAK after, only packed-refs. On main (107afae0) the same run reports 1 LEAK, also only packed-refs, so that one is local to macOS and not from this change.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] src/uninstall.ts:988 — installsSharingHome still skips unreadable configs. With another partition present but its config unreadable, this check authorizes removing shared hooks; repairing that config leaves the other installation without hooks. Preserve hooks when another partition’s config cannot be read, as the description promises.

  • [P2 non-blocking] src/dashboard-collector.ts:460 — The host-blocked result filtering, tests, marker, and changelog entry address Friction reminder counts Claude Code auto-mode classifier timeouts as failing tools #1821, not this PR’s uninstall goal. Move these changes to a separate PR to satisfy the rule requiring surgical changes tied to the stated goal.

Both earlier findings remain unresolved. The description documents sufficient representative real-CLI verification for removal and preservation. Nothing from the PR was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Reviewed at b27e94c4, with the CLI built at this head and at main (107afae0) and run in verify worlds. The last-install case does what #1825 asks: after a workspace uninstall --force, ~/.codex/hooks.json and the project's data home are gone, and main kept the file. b27e94c4 also fixes the self-keeping partition from b3144947. One blocker remains, and a few things need to go in before I can approve.

[P1 blocking] An unreadable config in another workspace removes that workspace's hooks. The body says "A partition whose config cannot be read counts as another install, so the failure mode is keeping, not removing." The code does the opposite. installsSharingHome drops a partition whose readConfigFrom returns null, so lastInstallOnMachine answers true. Repro: two workspaces with Codex; corrupt the second one's config.yaml under ~/.teamai/projects/<slug>; run uninstall --force in the first. At this head ~/.codex/hooks.json is removed; main keeps it. Once the second workspace's config is fixed, it has no hooks. The automated review raised the same thing. A partition directory whose config can't be read should count as another install, and that needs a test row.

[P2 non-blocking] A pre-partition install isn't counted. A workspace whose config is still <project>/.teamai/config.yaml is read by detection (config.ts:1109-1112), but installsSharingHome only scans ~/.teamai/projects/*. With such a workspace on the machine, uninstalling another one removes ~/.codex/hooks.json while teamai status in the old one still reports it installed. Its next pull writes the hooks back, so I'm not blocking on it. Either count it, or say in the CHANGELOG that an old-layout install on the same machine needs a pull afterwards. (#1817's wrapper sweep has the same gap.)

[P2 non-blocking] The CHANGELOG lists only part of what is now removed. globalAdapters gates more than the six adapters the entry names. On this path a workspace uninstall now also removes:

  • the Hermes status hook and plugin (uninstall.ts:1633, :1775);
  • the bridged team hook file (:776);
  • the OpenCode agent hooks (:586-600);
  • the Pi agent extensions (:667-674);
  • skills in earlier hermes/omp/opencode homes (:1345).

They're all teamai's own files, but a fix that starts deleting member-visible files has to list them.

Merge main. The branch conflicts on CHANGELOG.md now that #1824 is merged as e30376d2. Merging main also drops the #1824 commit from this diff, which settles the automated review's second point.

Not covered here: Windows, self mode, submodules and workspace children. Hermes, Pi, OMP, Kiro and Antigravity were checked by reading the code. Only Codex was run.

@Walternater
Walternater force-pushed the fix/last-install-hook-removal branch from b27e94c to f39d0ec Compare November 11, 2026 03:19
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] src/uninstall.ts:988 — lastInstallOnMachine still relies on installsSharingHome, which skips unreadable configs. If another partition’s config cannot be read, uninstall can delete its shared hooks; repairing that config leaves the installation without hooks. Preserve hooks in this case, as the description promises.
  • [P2 non-blocking] src/dashboard-collector.ts:462 — The host-blocked result filtering, tests, marker, and changelog entry address Friction reminder counts Claude Code auto-mode classifier timeouts as failing tools #1821, not this PR’s uninstall goal. Move them to a separate PR to satisfy the rule requiring surgical changes tied to the stated goal.

Both earlier findings remain unresolved. The description documents sufficient representative real-CLI verification for removal and preservation. Nothing from the PR was run, built, or installed.

@Walternater Walternater reopened this Nov 11, 2026
@Walternater

Copy link
Copy Markdown
Collaborator Author

CI did not start for f39d0ecb, and the same was true for b27e94c4 before it: on both heads the only check-runs are Authorize trigger, Codex PR Review and the codecc scan. The last pull_request run on this branch is b3144947, which is the failing one, and reopening the PR did not start another.

So the fix for the regression CI found is pushed but unmeasured by CI:

  • b3144947 failed in Agent Wait Budget (LEAK home/.teamai/projects/<slug>), and in Windows and all three E2E shards;
  • f39d0ecb gates keptForProjects() on the scope again, so a project uninstall on the last install removes its own data home, and the new case now asserts that;
  • locally, node scripts/agent-wait-budget.mjs on this head reports one leak, project/.git/packed-refs, which main (107afae0) reports too, so it is not from this change.

A re-run at the new head would let those four jobs confirm it. I cannot start a pull_request run from here.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

CI isn't starting because the branch conflicts with main. GitHub runs pull_request workflows on the test merge commit, and with CHANGELOG.md in conflict there isn't one (mergeable: false). Reopening doesn't change that. Merge main into the branch, which also drops the #1824 commit now that it's merged as e30376d2, and the full suite runs on the new head.

When you do, the review above still applies: the unreadable-partition case (a partition directory whose config can't be read should count as another install), and the CHANGELOG list of what a workspace uninstall now removes. The self-keeping partition fix in f39d0ecb looks right.

@Walternater
Walternater force-pushed the fix/last-install-hook-removal branch from f39d0ec to fc61a5f Compare November 11, 2026 03:27
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] src/uninstall.ts:989 — installsSharingHome still skips partitions whose configs cannot be read. With another partition present but its config unreadable, this check can authorize deleting shared hooks; repairing that config leaves the other installation without hooks. Preserve hooks when another partition’s config cannot be read, as the description promises. This earlier finding remains unresolved.

The earlier unrelated-change finding is resolved: the #1821 dashboard filtering, tests, marker, and changelog addition are absent from this diff.

The description documents sufficient representative real-CLI verification for both removal and preservation. Nothing from the PR was run, built, or installed.

@Walternater

Copy link
Copy Markdown
Collaborator Author

You are right about the P1, and the E2E suite turned up something bigger, so I am withdrawing this rather than fixing the P1 and leaving the rest.

On the P1. My body said "a partition whose config cannot be read counts as another install"; installsSharingHome does the opposite — it drops a partition whose readConfigFrom returns null — so lastInstallOnMachine answers true and removes the shared file. That is fixable by counting the partition on its config.yaml existing rather than parsing.

What makes me stop is the sibling checkout. Three shards fail on the same shape:

FAIL src/__tests__/e2e/codex-team-hook-dispatcher-915.test.ts > uninstall --agent codex
  AssertionError: expected '…' to contain 'Excluded codex from this project'
FAIL src/__tests__/e2e/submodule-checkout-1041.test.ts > … > uninstall --agent codex from a linked worktree
  keeps the main checkout's
  AssertionError: expected '' to be '{ "hooks": { "Stop": [ …' }

A linked worktree, a submodule checkout and a sibling clone of the same project share the current data home, so installsSharingHome excludes them by design, and the HOME ~/.codex/hooks.json is one dispatcher set serving all of them. That is the "another checkout" in the comment on globalAdapters, and it is the case my predicate cannot see. With the file removed, the checkout that is still set up loses its team hooks until its next pull — the failure the suite reports.

Why I am not widening it once more. Making the predicate checkout-aware means enumerating checkouts of the current project, which uninstall has no helper for (keptForProjects returns one row per data home; projectCheckouts is self mode only), and the error direction matters: removing is destructive, keeping is not. A one-boolean change that needs three new enumerations to be safe is not the right shape.

What the right shape looks like, for whoever takes it: remove at the record level — strip only this install's dispatcher from the shared file and delete the file only when no other record still names it — instead of deciding the file wholesale from a machine-wide boolean. That is a bigger change than this PR, and it would also settle your P2 about the pre-partition install.

I have put all of it — the sibling-checkout case, the unreadable-config rule, and the full list of what globalAdapters gates — into #1825, so the next attempt starts from the evidence instead of from the one-liner. The branch is reset to main; nothing to merge. Sorry for the round trip.

@github-actions

Copy link
Copy Markdown

No findings: the exact requested three-dot diff is empty.

e30376d23b90c85f042afbc9993cc248b97eb019 is the parent of 03099c9dba890eaf704dad044e2d0ef784945380, so the merge base equals the requested head. There are no PR changes to review in this range.

The supplied description includes sufficient representative real-CLI verification. The empty diff does not establish whether the earlier uninstall finding is fixed.

Please provide the correct base and head commits for #1826. Nothing was run, built, or installed from the PR.

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.

[bug] uninstall in a project keeps the machine-wide hooks it wrote, even when no other install is set up

3 participants