Skip to content

feat(cli): persist install hook opt-out - #1862

Open
Andy11-cpu wants to merge 7 commits into
DeusData:mainfrom
Andy11-cpu:persistent-install-hook-opt-out
Open

Andy11-cpu wants to merge 7 commits into
DeusData:mainfrom
Andy11-cpu:persistent-install-hook-opt-out

Conversation

@Andy11-cpu

Copy link
Copy Markdown
Contributor

Fixes #1857

Supersedes #753.

What

  • Keeps install hooks enabled by default.
  • Adds persistent install --no-hooks and install --hooks.
  • Applies the policy across current hook-capable installer surfaces.
  • Preserves MCP, skills, instructions, profiles, and foreign hook state.
  • Keeps plan/dry-run non-mutating.

Why

#753 established the performance cost of per-call hook process spawning. This implements the maintainer-approved direction from that discussion: retain graph-first defaults while giving users a persistent opt-out.

Verification

  • scripts/test.sh
  • scripts/lint.sh --ci CLANG_FORMAT=clang-format-20
  • scripts/ci/lint-mem.sh clang-tidy-22
  • make -f Makefile.cbm security
  • agent-config smoke
  • focused hook-policy tests
  • git diff --check

All pass locally.

@Andy11-cpu
Andy11-cpu requested a review from DeusData as a code owner August 27, 2026 20:18
@Andy11-cpu Andy11-cpu mentioned this pull request Aug 27, 2026
1 of 2 tasks
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added enhancement New feature or request editor/integration Editor compatibility and CLI integration ux/behavior Display bugs, docs, adoption UX priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Sep 1, 2026
@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Thank you for implementing the previously discussed persistent hook opt-out and for preserving foreign hook state, dry-run behavior, and the remaining installer surfaces. The contribution queue is currently quite full, so the full review may take a little time. We have the PR routed and will come back with grounded feedback as soon as possible.

@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reviewed. The direction is settled — this implements the approved outcome from #753 — so this is about one build break, and it is a small one.

The Windows failure is real, and it is a missing platform guard

Both test-windows shards, pr-smoke (windows-latest) and test-windows-guards fail for the same reason: the tree does not compile on Windows.

src/cli/cli.c:8846: error: call to undeclared function 'cbm_remove_gemini_coverage_hook'
                    did you mean 'cbm_remove_augment_coverage_hook'?

The compiler's suggestion is a red herring. cbm_remove_gemini_coverage_hook does exist — at src/cli/cli.c:5769 — but its definition sits inside #ifndef _WIN32, which opens a few lines above it. On Windows it is compiled out entirely.

The existing call site already knows this. At cli.c:11104:

#ifndef _WIN32
        if (cbm_remove_gemini_coverage_hook(cp, installed_binary) != CLI_OK) {
            ...
        }
#endif

Your new call is not wrapped. Adding the same #ifndef _WIN32 / #endif pair around it should be the whole fix.

Why only Windows caught it: clang treats an implicit function declaration as an error, and the CLANG64 legs are the ones building with clang. On POSIX the function is present, so every other leg compiles cleanly and says nothing. Worth knowing, because a green Linux and macOS run tells you nothing about this class of mistake.

One knock-on to check while you are there: cbm_upsert_gemini_coverage_hook lives under the same guard at :5753. If the persistent-policy work added an upsert call anywhere, it needs the same treatment — the compiler only reports the first error.

The rest

The shape is right: hooks stay enabled by default, --no-hooks / --hooks persist the choice, plan and dry-run stay non-mutating, and foreign hook state along with MCP, skills, instructions and profiles is preserved. Keeping the graph-first default while giving a persistent opt-out is what #753 concluded, and doing it as a persisted policy rather than a per-invocation flag is what actually solves the reported problem.

Clearance is REVIEW(1) on scripts/smoke-test.sh — the established shape here. It needs a maintainer marker, not a change from you.

Also please rebase once the guard is in: main moved three times yesterday (broken by a duplicate-symbol merge, repaired by #1993, then #1703 landed).

Signed-off-by: Andy11-cpu <canada11@duck.com>
Signed-off-by: Andy11-cpu <canada11@duck.com>
Signed-off-by: Andy11-cpu <canada11@duck.com>
@Andy11-cpu
Andy11-cpu force-pushed the persistent-install-hook-opt-out branch from fc203c6 to 0e8fc16 Compare September 2, 2026 04:22
Maintainer carry of DeusData#1862 (persistent install-hook opt-out, direction
approved in DeusData#753). Main's current behaviour is kept everywhere; only the
PR's intent -- persisting `install --no-hooks` / `--hooks` -- is
re-applied on top of it.

Textual conflicts:

- docs/CONFIGURATION.md: both sides appended rows to the "Current keys"
  table. Kept main's `index_max_files` and `index_max_source_mb` rows
  unchanged, then added the PR's `install_hooks` row after them. The PR's
  explanatory paragraph follows the table, so the table stays one
  contiguous block.

- src/main.c (print_help): main added a second `install` usage line for
  `[--clients=<tokens>]`; the PR appended `[--hooks|--no-hooks]` to the
  first line. Kept both: main's first line plus the PR's hook flags,
  followed by main's `--clients` line unchanged.

Semantic conflicts (merged cleanly as text, wrong against main):

- src/cli/cli.c, install_claude_code_config() --no-hooks branch: main
  now registers Claude hooks shell-free (DeusData#1733), as
  `<binary> hook-augment`, and changed cbm_remove_session_hooks() to take
  (settings_path, binary_path). The PR's disable path still called
  cbm_remove_session_hooks(settings_path), which no longer compiles, and
  used the NULL-binary cbm_remove_claude_hooks() /
  cbm_remove_claude_subagent_hooks() wrappers, which do not match the
  exec-form entries main installs. The disable path now calls the same
  binary-aware removers as main's uninstall_claude_code():
  cbm_remove_claude_hooks_with_binary(), cbm_remove_session_hooks(...,
  binary_path) and cbm_remove_claude_subagent_hooks_with_binary(), all
  with the binary_path the install side registers. Legacy shell-form
  entries are still covered through those functions' old_commands lists.

- tests/test_cli.c,
  cli_install_hooks_preference_persists_and_migrates_owned_state: the
  settings.json checks looked for "cbm-code-discovery-gate", but main no
  longer writes that name into settings.json (main's own
  cli_claude_hook_commands_use_exec_form_with_custom_config_dir asserts it
  is absent). The presence checks would be RED and the absence checks
  would pass without testing anything. They now use "hook-augment", the
  owned marker main's tests use. The script-file checks are unchanged,
  because main still writes the shim scripts.

- scripts/smoke-test.sh 8e-i: same stale marker. The --no-hooks check
  now also rejects "hook-augment" in settings.json, and the --hooks check
  requires it. Both checks use "$GATE_SCRIPT" (set per platform in 8e)
  in place of the extensionless shim path, which on Windows (.cmd) could
  never exist and so checked nothing there.

Everything else (cli.c hook gating, cli.h key, README, CONFIG_KEYS
table, test registrations) auto-merged against main without change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thanks again for the persistent hook opt-out. The recorded review asks remain the Windows guard around the Gemini helper call and a branch refresh, preserving the non-mutating plan/dry-run behavior. The smoke clearance mentioned above is a maintainer step, not something you need to change in the patch. Sorry for the delay on our smoke clearance; I’ll follow up on that soon.

Maintainer refresh of DeusData#1862 (persistent install-hook opt-out, direction
decided in DeusData#753) onto main a5b248f. The first refresh is 2577a86 and
carries the semantic fixes for shell-free Claude hooks.

Textual conflicts, all resolved by keeping both sides:

- src/cli/cli.h: main added CBM_CONFIG_DOC_CANDIDATES on the line where
  the PR added CBM_CONFIG_INSTALL_HOOKS. Both defines are kept, in
  alphabetical order.

- src/cli/cli.c CONFIG_KEYS[]: main added the watch_non_git row where the
  PR added install_hooks. Main row stays first, followed by the
  install_hooks row.

- src/cli/cli.c Codex hook install: main (DeusData#2719) declared pretool_written
  in front of the hooks.json branch the PR prefixed with its --no-hooks
  removal branch. The declaration stays first, then the PR removal
  branch, then main hooks.json and config.toml upsert branches unchanged.

Check: for every file the PR touches, the added and removed lines of the
merged tree against main equal those of the PR against its old base.

No compile-level semantic conflict: no function the PR calls changed its
signature, and the PR re-signs no function main calls. The new Codex
PreToolUse hook from DeusData#2719 is gated by the opt-out in the next commit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Since DeusData#1180 a bare ~/.claude directory no longer counts as a Claude Code
install. The fixture of cli_no_hooks_plan_covers_every_hook_installer_family
now also writes .claude/settings.json. Without it claude-code drops out of
agents_detected and the test fails on the tree merged with current main.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…-out

DeusData#2719 added a PreToolUse Bash hook for Codex after this opt-out was
written. In config.toml it lives inside the managed block, which the
opt-out already removes. In hooks.json it is a separate entry beside the
lifecycle pair, and the opt-out removed only the pair, so after
install --no-hooks, and on every later bare install or update that
respects the stored preference, our PreToolUse hook stayed registered.

The --no-hooks branch now also calls cbm_remove_codex_pretool_hooks_json,
the exact-command remover that uninstall uses since DeusData#2719. A PreToolUse
hook the user owns is left alone.

Test: cli_no_hooks_removes_codex_pretool_hook drives the real install
dispatch through both hook representations. A default install writes ours
beside a foreign hook; --no-hooks and a bare install with the stored
preference remove ours and keep the foreign hook and the MCP entry; and
--hooks restores ours. Without the fix it fails: ours is still in
hooks.json after --no-hooks.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

DeusData commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you, @Andy11-cpu, for carrying this all the way from #753. You did the hard part well. The opt-out is exhaustive: every hook-capable installer family is gated, not just Claude Code. The choice persists in the existing config store, so bare installs and update respect it. Foreign hooks and user-owned scripts are left alone, and plan and dry-run stay read-only. Your tests pin all of that through the same dispatch the real install uses, and the Windows guard you pushed within hours of the review was exactly the right fix.

main moved a long way while this waited, and that delay is on us. To spare you a painful rebase, we merged current main into your branch ourselves, as two merge commits plus two small commits on top of your three. None of your commits were rewritten. What changed and why:

  1. Claude Code hooks are now registered shell-free. Since this PR was opened, main writes Claude's hooks as <binary> hook-augment entries instead of shim-script commands (Windows: SessionStart hook injects a cmd.exe banner and the echoed payload instead of additionalContext #1733), and the session-hook remover now takes the binary path. So the --no-hooks path for Claude calls the same binary-aware removers that uninstall uses on main; without that, the new entries would not be recognized as ours.
  2. Test and smoke markers follow that change. Your settings.json checks looked for cbm-code-discovery-gate, which main no longer writes there. They now look for hook-augment, the owned marker main's own tests use, so the presence checks still bind. The smoke 8e-i checks use $GATE_SCRIPT, so the shim check is also real on Windows, where the file ends in .cmd.
  3. Three small conflicts, all resolved by keeping both sides. main added the doc_candidates and watch_non_git config keys next to your install_hooks, and a pretool_written flag in the Codex installer at the spot where your --no-hooks branch starts.
  4. Codex gained a PreToolUse hook while this waited (feat(cli): give Codex shell searches graph context through a PreToolUse hook #2719), and the opt-out now covers it. In config.toml the hook sits inside the managed block your opt-out already removes. In hooks.json it is a separate entry next to the SessionStart/SubagentStart pair, so --no-hooks now also removes it, using the same exact-command remover uninstall uses. A user's own PreToolUse hook stays. A new test, cli_no_hooks_removes_codex_pretool_hook, follows your tests' pattern and drives the real install through both representations: a default install, --no-hooks, a bare install with the stored preference, and --hooks.
  5. One fixture follows a detection fix on main. Since The install script failed to detect Hermes(Desktop) and OpenCode(Desktop) on Windows #1180, a bare ~/.claude directory no longer counts as a Claude Code install, because other tools create it too. Your plan test now also writes .claude/settings.json, so claude-code is still detected and the test still covers every family.

Because the branch now has new commits from us, please pull before you push again.

Thanks again for building an opt-out that keeps the graph-first default for everyone else. It is a real improvement for the people who need it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

editor/integration Editor compatibility and CLI integration enhancement New feature or request priority/normal Standard review queue; useful PR with ordinary maintainer urgency. ux/behavior Display bugs, docs, adoption UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): persistent opt-out for install hooks

2 participants