Repository navigation
feat(agent-isolation): add the overcommit hook-manager adapter - #1893
Conversation
Why: overcommit reads core.hooksPath unscoped and installs into whatever it names, then re-checks its install on every hook run. Under the whole-user setup that would replace the dispatcher for every repository on the host and then fail behind it, so it needs both an install strategy and a run-time override that prek's --git-dir flag does not cover. The new hooks-path-env strategy runs the manager's install with GIT_CONFIG_* pointing core.hooksPath at the repository's own <common dir>/hooks, and the dispatcher re-exports the same override before chaining into a hook that carries the adapter's marker (registry install.runtime_env). Refs apache#1821. Generated-by: Buffy (DeepSeek V4.1 Flash)
0c7f662 to
b921186
Compare
There was a problem hiding this comment.
Thanks @TejasS1233 for working on this. The implementation cleanly addresses the acceptance criteria in #1821 and adheres to the hook manager contract.
Code generation is consistent, documentation updates are comprehensive, and including the empirical reproduction baseline under the Verified runs section in tools/hook-manager/README.md is appreciated.
I have gone through the diff and verified the changes locally (sync-hook-managers.py with check flag passes with 0 drift, test_sync_hook_managers.py passes 28/28, licence headers and markdownlint checks are clean).
A few observations and notes for discussion:
1. Short option bypass (-i)
In tools/agent-isolation/hook-manager-shim.sh:
if [ "${1:-}" != "$install_subcommand" ]; then
exec "$real_tool" "$@"
fiOvercommit supports -i as the short alias for the install action flag. Because registry.json declares the long install flag, invoking overcommit -i will bypass the shim and execute the real binary directly against the shared hooks directory. We should either note in the documentation that the long flag form is required, or consider whether the shim should handle alias flags.
2. Option ordering
The check assumes the install command is always $1. With tools where the action is a flag rather than a positional subcommand verb, options can sometimes precede the action (for example overcommit -f followed by the install option). Any leading modifier will cause the invocation to bypass the shim.
3. Overwriting GIT_CONFIG_* variables
In both hook-manager-shim.sh and git-hook-dispatcher.sh:
export GIT_CONFIG_COUNT=1
export GIT_CONFIG_KEY_0=core.hooksPath
export GIT_CONFIG_VALUE_0=...Setting GIT_CONFIG_COUNT=1 overwrites any pre-existing git configuration overrides that a caller environment may have defined. This matches the design in #1821 and is standard for hook isolation, but is worth keeping in mind.
4. Defensive guard in hook-manager-shim.sh
if [ -n "${install_flag:-}" ]; then
case "$_a" in
"$install_flag"|"$install_flag"=*) inject=0; break ;;
esac
fiGood catch guarding ${install_flag:-} to prevent unbound variable errors under set -u when adapters have no long flag defined.
5. Cross-PR Coordination with Step P.3b Evals
PR #1892 introduces behavioral eval coverage for Step P.3b in setup-isolated-setup-install. Once both PRs land on main, we should add an eval case covering Overcommit detection (.overcommit.yml triggering installation of ~/.claude/bin/overcommit).
potiuk
left a comment
There was a problem hiding this comment.
The hooks-path-env strategy and the dispatcher's run-time override fit the contract well and are checked against real overcommit; four points on how far the rewrite and the exported override reach, and the spec, before it lands.
GitHub has not yet computed whether this branch merges cleanly with its base.
Smaller observations
tools/agent-isolation/hook-manager-shim.sh:104— overcommit's install is an option, not a positional subcommand: it also takes-i, and accepts it after other options (overcommit --force --install). Both skip the rewrite, reach the real binary unchanged, and install into the shared~/.claude/git-hooks/, which is the host-wide failure this adapter exists to prevent. For a flag-style install, scan all of argv for the flag and its aliases (e.g. aninstall.aliaseslist in the registry), or at least say in the adapter row that only a leading--installis rewritten.- See inline comments on
tools/agent-isolation/git-hook-dispatcher.sh:165,tools/hook-manager/registry.json:69,tools/agent-isolation/git-hook-dispatcher.sh:167.
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. The findings
below are observations, not blockers; an Apache Magpie
maintainer — a real person — will take the next look at the
PR. If you think a finding is mis-applied, please reply on
the PR and a maintainer will weigh in.More on how Apache Magpie handles maintainer review:
Contributing guide.
|
Thanks for the review. Addressed all four points in eaea149:
Verification: The #1892 eval coverage remains a post-merge follow-up once both PRs land. |
…alls overcommit parses its options with Ruby's OptionParser, which takes a single-letter switch inside a bundle (`overcommit -fi`) and an unambiguous prefix of a long one (`overcommit --inst`). The shim matched the install flag and its aliases by exact string, so both spellings reached the real binary unchanged and installed into the shared hooks directory. The shim now also matches a single-letter alias inside a short-switch bundle and a long flag shortened to three letters or more; rewriting a non-install run only points core.hooksPath at the repository's own hooks. Tests cover both spellings and pin that --run, --uninstall and the ambiguous --in still pass through. The Step P walkthrough, the hook-manager contract and the spec say the same, and the secure-setup fingerprint is restamped. Generated-by: Claude Opus 5
…n install A wrongly rewritten run only points core.hooksPath at the repository's own hooks, so fail closed on every OptionParser abbreviation (--i, --in, ...) instead of requiring three letters. Generated-by: Claude Opus 5
# Conflicts: # plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py
potiuk
left a comment
There was a problem hiding this comment.
eaea149 addresses all four points from the earlier review: the argv/alias scan, appending core.hooksPath at the next free GIT_CONFIG_* index (with tests), the documented reach of the exported override, and the spec. One gap was left, so I pushed it as fixup commits rather than send it back:
tools/agent-isolation/hook-manager-shim.sh(7685671, ef134d6): overcommit parses its options with Ruby's OptionParser, which also takes a single-letter switch inside a bundle (overcommit -fi) and any unambiguous prefix of a long one (overcommit --inst); both reached the real binary unchanged and installed into the shared hooks directory. The shim now matches those too, failing closed on any prefix of--install, since a wrongly rewritten run only pointscore.hooksPathat the repository's own hooks. New cases intests/test_hook_manager_shim.pycover-fi,--i,--instand--force --insta, and pin that--run,--uninstalland--still pass through.step-p-sandbox-allowlists.md,tools/hook-manager/README.mdand theagent-isolation-sandboxspec now describe the same matching, and the secure-setup fingerprint is restamped.- b1fb779 merges
main, which had moved the fingerprint constant; it is regenerated for the merged tree.
Please pull before pushing again.
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.More on how Apache Magpie handles maintainer review:
Contributing guide.
main now carries the overcommit adapter (apache#1893). The shim dispatches on all three install strategies (git-dir-flag, hooks-path-env and hide-global-config), the registry validator accepts each, and the docs and spec list prek, overcommit and lefthook together. The secure-setup fingerprint is main's, restamped by the hook. Generated-by: Claude Opus 5
Summary
core.hooksPathunscoped and installs into whatever it names, then re-checks that install on every hook run. A bareovercommit --installunder whole-user scope writes into~/.claude/git-hooks/andpreserve_old_hooksmoves the dispatcher's symlinks aside; and even a correct install fails behind the dispatcher, because the run-time self-check looks for its hook at the shared path and getsENOENT. So it needs both a newhooks-path-envinstall strategy and a run-time override that prek's--git-dirflag does not cover.tools/hook-manager/registry.json; the shim and dispatcher blocks are generated from it bytools/dev/sync-hook-managers.py, which gains thehooks-path-envstrategy, a subcommand that may be a flag (--install), and theinstall.runtime_envfield. The run against the real manager is recorded in the contract's new Verified runs subsection.Issue coverage
All four items of the issue are done, and each acceptance criterion was exercised against the real manager (see Test plan).
hooks-path-envinhook-manager-shim.shand inSTRATEGIES;install.subcommandmay be a flag, and the prek-onlyinstall.flagis now read safely underset -ucore.hooksPathoverride before chaining into a hook whose marker matches an adapter withinstall.runtime_envovercommitadapter inregistry.json, blocks regenerated, shim + dispatcher tests addedtools/hook-manager/README.mdType of change
tools/<system>/*.md)tools/*/withpyproject.toml)docs/,README.md,CONTRIBUTING.md)prek, workflows, validators)Test plan
Run inside a Linux environment (Ubuntu 24.04, Ruby 3.2.3, overcommit 0.73.0 — the version the issue analysed), against a simulated whole-user setup: global
core.hooksPathat the shared dir with the dispatcher symlinked into it, the shim installed as~/.claude/bin/overcommit.overcommit --installthrough the shim reportsInstalling hooks into <repo>; the hook lands in the repository's own.git/hooks/carrying theovercommitmarker;~/.claude/git-hooks/is byte-identical before and after (noovercommit-hook, noold-hooks/, globalcore.hooksPathunchanged, no repository-levelcore.hooksPath)..overcommit.ymlwith no self-update failure: a clean commit succeeds, and a trailing-whitespace commit is rejected by overcommit'sPreCommitTrailingWhitespacehook — so the manager really ran rather than the commit merely passing.overcommit --installwith no shim installs into the shared dir and moves the dispatcher's symlinks aside (Hook '…/pre-commit' already exists and was not installed by Overcommit. Moving to '…/.claude/git-hooks/old-hooks'), and a correct install under the dispatcher fromorigin/maindies in overcommit's own check (No such file or directory @ rb_file_s_size - ~/.claude/git-hooks/overcommit-hook,installer.rb:59).tools/agent-isolationsuite runs on Linux: the two files this change touches pass (26 tests), and the full suite's failures intest_sandbox_error_hint.pyare identical onorigin/main, so they pre-date this change.tools/dev:ruff check,ruff format --check,mypyclean anduv run pytestpasses, including the two new registry cases (a--flagsubcommand is accepted; a non-booleanruntime_envis refused).tools/sandbox-lint:uv run pytestpasses.tools/dev/sync-hook-managers.py --check→ exit 0, so the generated blocks matchregistry.json.prek run --all-files --skip workspace-pytest— running; CI runs it on this PR regardless.RFC-AI-0004 compliance
GIT_CONFIG_*override.Linked issues
Closes #1821.
Notes for reviewers (optional)
docs/setup/sandbox-troubleshooting.mdis shaped around sandbox write/read and network blocks with asettings.jsonwidening as the fix, and overcommit's residual failure modes are not blocks — its "shim not on PATH" consequence is already stated once, for every adapter, inisolated-setup-installStep P.3b-whole-user, and check 19d covers homes, which overcommit does not have. Happy to add an entry anyway if you would rather have it there.isolated-setup-doctorcheck that flags a foreign hook sitting in the shared~/.claude/git-hooks/(the leftover of an install made before the adapter existed) would catch the reverse of this change. Out of scope here; say the word and I will file it.PATHrecurse. The shim skips itself by canonical path, so a second copy (for example~/.claude/bin/overcommitplus a~/.claude-config/bin/overcommitsymlink, which Step P offers) is executed as if it were the real binary and bounces back. Worth a guard and an issue of its own; I have not touched it here.install.runtime_envis validated to be a boolean and to be settable only withhooks-path-env, so a later adapter cannot ask for the run-time override from a strategy that does not produce one.Generative AI disclosure: prepared with Buffy (DeepSeek V4.1 Flash); reviewed by the submitter.