tools/hook-manager/
Capability: contract:hook-manager
Kind: interface
Vendor: agnostic
This file defines the contract for git-hook managers — prek, pre-commit, lefthook, husky and their kin — under the secure agent setup.
A hook manager installs a script into a repository’s hooks directory and, when git runs that hook, runs the checks the repository configured, often from environments it built and keeps outside the repository.
Both halves meet the sandbox: where the install writes, and where the environments live.
The contract states the two rules every manager must satisfy, and registry.json records, per manager, how it satisfies them.
The secure setup does not run hook managers; it keeps them working, and keeps them from becoming a way out of the sandbox. It configures a manager for a repository only where the repository uses it (see Detection): a repository that uses none gets no shim and no sandbox-home requirement. The protections of the per-user default and host homes apply everywhere.
Prerequisites
- Runtime: None of its own — this file is a contract specification plus a JSON registry.
The implementations are shell scripts (
tools/agent-isolation/) and Python (tools/sandbox-lint/). - CLIs:
git; the hook manager itself is the operator’s. - Credentials / auth: None.
- Network: None.
Why a contract
Under the whole-user flavour of the secure setup, git config --global core.hooksPath ~/.claude/git-hooks/ makes git read hooks from one shared directory, where the dispatcher runs the framework’s own hook logic and then chains to the repository’s own $(git rev-parse --git-common-dir)/hooks/<name>.
Separately, the sandbox makes ~/.cache/ writable, and most hook managers keep their environments there.
Each manager meets those two facts differently — one writes into whatever core.hooksPath names, one refuses to install while it is set, one sets its own — so each needs its own fix, and the fixes must not drift apart.
The contract names the rules; the registry is the one place a manager’s answers are written down; the implementations are generated from it.
The rules
Install target
A manager’s install must put its hook in the repository’s own $(git rev-parse --git-common-dir)/hooks/, never in the shared core.hooksPath directory, and must not leave a repository-level core.hooksPath behind.
- The shared directory belongs to the dispatcher. A manager that writes there replaces the dispatcher for every repository on the host.
- A repository-level
core.hooksPathoverrides the global one, so git never reaches the dispatcher, and with it the host-only home below. magpie-setup’s session-start hook removes one that points at the repository’s own.git/hooksand reports any other value. --git-common-dir, not--git-dir: a linked worktree shares its main checkout’s hooks.
The fix is an install strategy, applied by hook-manager-shim.sh, installed as ~/.claude/bin/<tool> ahead of the real binary on PATH.
The shim takes the tool from the name it is installed under, rewrites only the install action, and passes every other invocation through unchanged.
A positional action (prek install) is only the first argument; a flag-style action (overcommit --install) and its registry aliases (overcommit -i) are found anywhere in argv, including a single-letter alias inside a bundle of short switches (-fi) and any shortening of a long flag (--inst).
An adapter sets install.match: prefix for a manager whose option parser takes global options before the command and resolves a prefix of it, so captainhook -n install and captainhook inst count as well; any non-option argument that prefixes the action matches, and rewriting a run only points core.hooksPath at the repository’s own hooks, which is harmless.
An adapter sets install.vendor_bin for a manager that Composer installs into a project’s vendor/bin, which is not on PATH; when the PATH walk finds no real binary the shim runs <repository root>/vendor/bin/<tool>, and a symlink back to the shim is refused rather than exec’d. A global binary on PATH always wins over the project’s pinned one.
It leaves the install alone when the caller already chose a target, asked for help, or is not in a git work tree.
| Strategy | What the shim does | Status |
|---|---|---|
git-dir-flag |
Inserts <flag> <common dir> after the install subcommand (prek: --git-dir; prek refuses a bare install while a global core.hooksPath is set). |
implemented |
hide-global-config |
Runs the install with GIT_CONFIG_GLOBAL=/dev/null whenever the global config holds a core.hooksPath, for a manager that writes to the common dir but refuses while one is set (pre-commit, lefthook); a host without one keeps its global credential.helper / url.*.insteadOf settings. The override also applies to an install carrying --force or --reset-hooks-path, so neither can reach the shared directory through the shim; run the real binary to do that. |
implemented |
hooks-path-env |
Runs the install with core.hooksPath=<common dir>/hooks in the next free GIT_CONFIG_* slot, preserving a caller’s existing entries, for a manager that writes into whatever core.hooksPath names (overcommit, CaptainHook). The value must be that absolute path: git treats an empty core.hooksPath as set, and hooks then resolve against the filesystem root. The run-time override is exported, so git commands the chained hook starts inherit it, including ones aimed at another repository; only the manager’s self-check needs it, so that leak is a known limitation. |
implemented |
none |
The manager already writes to the repository’s own hooks directory whatever the global value (simple-git-hooks, cargo-husky, the JGit-based Maven plugins). | implemented |
A manager whose install the shim cannot reach (one run from node_modules/.bin by an npm script, from vendor/bin by a composer plugin, or from a build.rs, all ahead of ~/.claude/bin or off PATH entirely) needs a strategy outside the shim, and a manager that writes a repository-level core.hooksPath itself (husky on every npm install) needs the dispatcher to chain to that directory instead; the adapter issues name the candidates.
Home
A hook that runs outside the sandbox must never run an environment a sandboxed command could have written.
A manager that keeps environments in a home outside the repository gets three directories:
| Directory | Who uses it | How |
|---|---|---|
default (under ~/.cache/) |
The operator’s terminal under per-project scope. | Sandboxed commands may not edit it: permissions.deny carries Edit(<default>/**). |
sandbox (under ~/.cache/, disjoint from default) |
Every command Claude Code runs. | The committed settings’ env.<var>, which only Claude Code’s commands see. |
host (one directory under ~/.local/share/) |
Every hook the whole-user dispatcher runs outside the sandbox (no SANDBOX_RUNTIME). |
The dispatcher exports <var>=~/.local/share/<host> for every registered manager unless the operator chose a value outside ~/.cache. |
~/.local/share/ is outside every sandbox write grant, so nothing sandboxed can plant code in the host home.
The committed settings can name a per-user directory only with ~, so the contract requires a manager whose home variable is relative to ~ to expand it (expands_tilde: true); one that does not needs the sandbox home written as an absolute path into the per-user settings.local.json, which no adapter does yet.
Detection
A repository uses a hook manager when any of these holds:
- One of the manager’s config files sits at the repository root.
- For a manager with nested project configs (prek’s workspace mode), one of its config files is tracked anywhere below the root (
git ls-files ':(glob)**/<name>'). An untracked nested file does not count. - One of the repository’s installed hooks (
$(git rev-parse --git-common-dir)/hooks/*, samples aside) contains the manager’s hook marker. This covers an install with a custom config path (prek install --config ci/hooks.yamlleaves no standard file name behind) and one made from a nested project.
Detection decides what the setup configures for a repository; it never decides a per-user protection, since a manager’s default and host homes are shared by every repository on the host, and a hook can reach a manager detection does not see (through a script):
| Integration | Scope | Rule |
|---|---|---|
Sandbox home (env.<var>) |
per repository | sandbox-lint (uses_hook_manager) requires it only for a project that uses the manager; with no project to look at, every manager counts. A project without it is still safe: its sandboxed runs of the manager fail on the denied default home. The generator checks the framework’s own settings the same way, from the root config files. |
| Shim | per host, for the repositories the operator works in | setup-isolated-setup-install installs ~/.claude/bin/<tool> only when such a repository uses the manager, and only for adapters with an install strategy other than none (managers requiring no install fix need no shim). |
| Verify check 19d | per repository | n/a for a project that does not use prek. |
Default-home deny (Edit(<default>/**)) |
per user | Always, for every registered manager: another repository’s terminal commits run from that home. |
| Host home (dispatcher) | per user | Always, outside the sandbox, for every registered manager: an unused variable is harmless, a missed detection would let a bypassed command inherit the sandbox home. |
How prek configures itself, from its source (v0.5.7): it reads one project config per directory, prek.toml, then .pre-commit-config.yaml, then .pre-commit-config.yml (crates/prek-consts/src/lib.rs); searches upward from the current directory to the git root for the workspace root, and below it for nested project configs, honouring .gitignore and .prekignore (crates/prek/src/workspace.rs); takes --config <path> instead, which disables discovery and is baked into the installed hook; and reads a user-level prek.toml under the platform config directory that holds only update and display options, so it says nothing about whether a repository uses prek.
The two YAML names are shared with pre-commit, so a repository with one uses both managers as far as detection goes; configuring a home the repository’s hooks do not end up using is harmless.
How lefthook configures itself, from its source (v2.2.2): it reads lefthook, .lefthook or .config/lefthook with one of .yml, .yaml, .json, .jsonc, .toml, and the same three names with a -local suffix as the repository’s local override (internal/config/loader.go).
Only the bare root names are in the registry: the contract’s detect.config_files holds file names, not directories, so a .config/lefthook.yml at the repository root is not detected by rule 1; the installed hook’s call_lefthook marker still catches it by rule 3.
lefthook install writes to $(git rev-parse --git-path hooks) and refuses while a scoped git config --local or --global read returns any core.hooksPath (internal/command/install.go).
GIT_CONFIG_GLOBAL=/dev/null hides the global value from that read, and the path then resolves to the common directory, linked worktrees included.
Remote configs are cloned into <git info>/lefthook-remotes, inside .git, so the registry omits home.
How CaptainHook configures itself, from its source (v5.29.2): it reads captainhook.json at the repository root, or the path given with --configuration.
Repository::getHooksDir() reads git config --get core.hooksPath (Command/Config/Get.php:53), uses an absolute value as-is, and refuses only /dev/null (Installer.php:291-296), so under the whole-user setup an unshimmed install writes into the shared dispatcher directory.
A relative value is joined to the repository root; in a linked worktree with no core.hooksPath the hooks dir is built from the worktree’s absolute gitdir, which does not exist (Repository.php:88-101).
The hooks-path-env override fixes both, because the install then reads the common dir’s hooks as an absolute value.
Its CLI is a Symfony Console application, so install may follow global options (captainhook -n install) or be an unambiguous prefix (captainhook inst); that is what install.match: prefix covers.
Composer keeps the binary in vendor/bin, which is not on PATH, so install.vendor_bin lets the shim fall back to <repository root>/vendor/bin/captainhook; ~/.claude/bin/captainhook install therefore works from anywhere in the repository, including a linked worktree.
The captainhook/hook-installer and captainhook/plugin-composer Composer plugins still install on every composer install and composer update, outside the shim.
The documented path is one explicit ~/.claude/bin/captainhook install from the repository root after a clone, with extra.captainhook.disable-plugin set to true in composer.json so the plugin does not install into the shared directory behind the shim’s back.
An install through the shim warns when composer.json requires either plugin and neither that key nor config.allow-plugins turns it off; the check reads the keys with python3 rather than matching text, so a disable-plugin under another package’s extra cannot silence it, and is skipped when python3 is not on PATH.
The generated shell and PHP templates run the binary and configuration through repository-relative paths and rely on git running hooks from the top level, which the dispatcher keeps; the PHP template additionally derives .git from the hook file’s location (Local/PHP.php:84), so check it from the common dir in a linked worktree.
CAPTAINHOOK_SKIP_HOOKS=1 skips a run.
Registry
registry.json holds one adapter per manager under adapters.<binary name>:
| Field | Meaning |
|---|---|
language |
The ecosystem the manager comes from (for the reader, not the implementation). |
upstream |
The manager’s source repository. |
detect.config_files |
The config file names that say a repository uses it (bare names, no directories). |
detect.nested |
Whether a tracked config below the root counts (rule 2 of Detection). |
detect.hook_marker |
Text every hook the manager installs contains (rule 3). |
install.subcommand |
The subcommand or flag the shim rewrites. |
install.aliases |
Other spellings of a flag-style subcommand (overcommit: -i), matched anywhere in argv. |
install.strategy |
One of the install strategies above. |
install.flag |
For git-dir-flag: the long option that takes the git directory. |
install.match |
How a positional subcommand is recognised: exact (the default, argv[0] is the action) or prefix (any non-option argument that prefixes it, for a Symfony Console app that takes global options before the command). |
install.vendor_bin |
true for a manager Composer installs into a project’s vendor/bin: the shim falls back to <repository root>/vendor/bin/<tool> when PATH holds no real binary. |
install.runtime_env |
For hooks-path-env: also give every hook run the same core.hooksPath override, so a manager that re-checks its install at run time accepts the chained hook. |
home |
Omitted for a manager that keeps nothing outside the repository. |
home.env |
The environment variable that relocates the home. |
home.default, home.sandbox, home.host |
The three directories above. |
home.expands_tilde |
Whether the manager expands a leading ~ in home.env; must be true today. |
tools/dev/sync-hook-managers.py validates the registry against the rules and writes it into the implementations, which run where the registry is not available, as generated blocks:
tools/agent-isolation/hook-manager-shim.sh— the install rewrite per tool;tools/agent-isolation/git-hook-dispatcher.sh— the host home and run-time override per tool;tools/sandbox-lint/—HOOK_MANAGER_HOMES, which fails any settings file whose deny list lacksEdit(<default>/**), and a project’s settings file whoseenv.<var>is missing or overlaps the default home when the project uses the manager.
It also checks, without writing, that .claude/settings.json and tools/sandbox-lint/expected.json deny every manager’s default home and carry the sandbox home of each manager this repository uses.
It runs as the sync-hook-managers prek hook.
Operations
The contract has no binary of its own. Its operations are carried out by the scripts generated from the registry:
| Operation | Run by | Invocation | Behaviour |
|---|---|---|---|
| Install rewrite | hook-manager-shim.sh, installed as ~/.claude/bin/<tool> |
<tool> [args…], e.g. prek install, lefthook install, overcommit --install (also -i, or after other options) or captainhook -n install (also inst) |
Applies the adapter’s install strategy: git-dir-flag runs prek install --git-dir <absolute common dir> [args…]; hooks-path-env runs overcommit --install or -i, or captainhook install in any of its spellings, with core.hooksPath pointed at <absolute common dir>/hooks, appending to any existing GIT_CONFIG_* entries; hide-global-config runs the install with GIT_CONFIG_GLOBAL=/dev/null when the global config holds a core.hooksPath. Left unchanged when args already carry the target flag or ask for -h / --help, or outside a git work tree; a hide-global-config install carrying --force or --reset-hooks-path still gets the override, so neither reaches the shared directory through the shim. Every other subcommand passes through to the real binary, the next <tool> on PATH that is not the shim, or <repository root>/vendor/bin/<tool> for an adapter with install.vendor_bin. Exits 127 when the name it is installed under has no adapter or no real binary is found. |
| Host home and run-time override | git-hook-dispatcher.sh, from ~/.claude/git-hooks/ |
Every hook git runs, before it chains to the repository’s own hook | Outside the sandbox (no SANDBOX_RUNTIME), exports <home.env>=~/.local/share/<home.host> for every registered manager unless the operator’s value lies outside ~/.cache, and moves XDG_CACHE_HOME to ~/.local/share/prek-host/cache by the same rule. Sandboxed hooks keep their values. Before chaining to a hook whose marker matches an adapter with install.runtime_env, exports the same core.hooksPath override the shim used, so the manager’s own run-time check accepts the hook. |
| Home check | sandbox-lint |
uv run --project tools/sandbox-lint sandbox-lint |
Fails a settings file whose permissions.deny lacks Edit(<home.default>/**) for any registered manager, and, for a project that uses a manager (see Detection), one whose env.<home.env> is missing or overlaps the default home. |
| Registry sync | tools/dev/sync-hook-managers.py |
tools/dev/sync-hook-managers.py [--check]; the sync-hook-managers prek hook |
Validates registry.json, rewrites the generated blocks of the three scripts above, and checks the committed settings and the sandbox-lint baseline. Exits 1 when a block changed (re-stage and commit), or, with --check, when one would. |
Configuration
The registry is framework-side: an adopter does not edit it. What a repository or an operator configures is where each manager’s environments live and which binary answers to the manager’s name:
| Setting | Where | Value for prek | Required |
|---|---|---|---|
permissions.deny entry Edit(<home.default>/**) |
.claude/settings.json (and tools/sandbox-lint/expected.json in this repository) |
Edit(~/.cache/prek/**) |
Always, for every registered manager. |
env.<home.env> |
.claude/settings.json |
"PREK_HOME": "~/.cache/prek-sandbox" |
Only when the repository uses the manager; it must not overlap the default home. |
<home.env> in the operator’s environment |
the shell the operator commits from | unset, or a directory outside ~/.cache |
Optional: a value outside ~/.cache is kept by the dispatcher; one under it is replaced with the host home. |
PATH |
the operator’s shell profile | ~/.claude/bin ahead of the real prek |
Only under whole-user scope, where the shim is installed; removing ~/.claude/bin from PATH restores the stock binary. |
setup-isolated-setup-install brings in the first two with its copy of the framework’s .claude/settings.json and the PATH entry in Step P; sandbox-lint checks the first two.
Adapters
| Adapter | Language | Install strategy | Home | Status |
|---|---|---|---|---|
captainhook |
PHP | hooks-path-env (install, matched as a prefix at any non-option argument); detected by captainhook.json or a hook mentioning CaptainHook; vendor/bin fallback |
none (no external home) | shipping (verified: v5.29.2 on PHP 8.5.12 with Composer 2.11.1; the install through the shim uses the vendor/bin fallback and writes the repository’s own hooks, and a bare install replaces the shared dispatcher) |
cargo-husky |
Rust | none; detected by a hook mentioning cargo-husky |
none (no external home) | shipping (in linked worktrees build.rs skips hook install: searches for a .git directory) |
git-build-hook |
Java | none; detected by pom.xml (nested too) or a hook mentioning git-build-hook |
none (no external home) | shipping (JGit getGitDir() targets <worktree>/hooks in linked worktrees; configure goal can set repo-local core.hooksPath) |
git-code-format-maven-plugin |
Java | none; detected by pom.xml (nested too) or a hook mentioning git-code-format-maven-plugin |
none (no external home) | shipping (JGit getCommonDirectory() targets common hooks dir, coexisting cleanly in linked worktrees) |
lefthook |
Go | hide-global-config; detected by lefthook.yml, lefthook.yaml, lefthook.json, lefthook.jsonc, lefthook.toml, their .lefthook and -local variants, or a hook mentioning call_lefthook |
none: remote configs live under <git info>/lefthook-remotes, inside .git |
shipping |
prek |
Rust | git-dir-flag (--git-dir); detected by prek.toml, .pre-commit-config.yaml or .pre-commit-config.yml (nested too) or a hook mentioning prek |
PREK_HOME: ~/.cache/prek → sandbox ~/.cache/prek-sandbox, host ~/.local/share/prek-host |
shipping |
overcommit |
Ruby | hooks-path-env (--install, alias -i, found anywhere in argv); detected by .overcommit.yml or a hook mentioning overcommit |
none — it keeps its own state in git config and the repository | shipping |
simple-git-hooks |
JavaScript | none; detected by package.json, .simple-git-hooks.{js,cjs,json}, simple-git-hooks.{js,cjs,json} or a hook mentioning simple-git-hooks |
none (no external home) | shipping |
Ecosystem notes:
- Behavior and verification under whole-user dispatcher:
simple-git-hooks(v2.14.0, empirically verified): Installs to$(git rev-parse --git-common-dir)/hooks/directly, ignoring globalcore.hooksPath. Commits chain through the dispatcher to the local hook. In linked worktrees, commits chain through the dispatcher to the common hooks directory, and running install within a worktree resolvescommondirwithout error.cargo-husky(v1.5.0, empirically verified): Installs to.git/hooks/pre-pushviabuild.rs. Commits and pushes chain through the dispatcher. In a linked worktree,build.rsexpects.gitto be a directory. Encountering a worktree.gitfile, it logsWarning: .git directory was not foundto stderr and skips hook installation without failing the build. Detection relies solely on installed hook markers (config_files: []) so generic Rust checkouts withCargo.tomlare not falsely detected ascargo-huskyrepos.git-build-hook(v3.6.0, verified from upstream source): Installs hooks directly into JGit’sRepository.getGitDir() + "/hooks"(InstallMojo.java:53). Commits in the main checkout chain through the dispatcher. In linked worktrees,getGitDir()evaluates to<commondir>/worktrees/<name>, placing hooks in<worktree>/hooks/which C-git ignores on commit. Runningmvn git-build-hook:configurecan write a repo-localcore.hooksPath, overriding the dispatcher.git-code-format-maven-plugin(com.cosium.code:git-code-format-maven-plugin5.3, verified from upstream source):AbstractMavenGitCodeFormatMojo.getOrCreateHooksDirectory()usesgitRepository().getCommonDirectory().toPath().resolve(HOOKS_DIR). Because it targets the common directory, hooks land in$(git rev-parse --git-common-dir)/hooks/and coexist cleanly in linked worktrees. Installs a bash wrapper invokingmvn git-code-format-maven-plugin:format-code.lefthook(v2.2.2, verified against the release binary through the shim): Refuses a bare install while a scopedgit config --globalread returns anycore.hooksPath; the shim’sGIT_CONFIG_GLOBAL=/dev/nullhides it, and the install then writes to$(git rev-parse --git-path hooks), the common hooks directory.--forceand--reset-hooks-pathstill get the override through the shim, so neither reaches the shared directory or unsets the setup. In a linked worktree the same install lands in the main checkout’s.git/hooks/. npm’spostinstallrunsnode_modules/.bin/lefthookahead of~/.claude/bin, so the shim does not cover it: that install refuses, and the repository needs one run through the shim. A fix would be the session-start hook noticing hooks in the shared directory that are not the dispatcher; the shim cannot reach an npm-run child ahead of it onPATH.
- JGit in linked worktrees (Maven):
The linked-worktree limitation applies only to
git-build-hook(InstallMojo), which usesgetGitDir()and creates hooks under<commondir>/worktrees/<name>/hooks/. Because C-git commits read hooks exclusively from$(git rev-parse --git-common-dir)/hooks/, hooks installed bygit-build-hookfrom within a worktree are ignored by Git. Conversely,git-code-format-maven-pluginexplicitly callsgetCommonDirectory(), so its hooks populate the common directory and function across worktrees. - Gradle & Go hook tools: Standard Gradle plugins and other Go hook utilities remain unadapted and unregistered until someone tests them under the dispatcher.
Verified runs
An install strategy is written from the manager’s source. Where a run against the real manager confirmed it, the run is recorded here, with the controls that show what fails without the adapter.
overcommit, v0.73.0 on Ruby 3.2.3, Ubuntu 24.04, whole-user dispatcher.overcommit --installthrough the shim wrote<common dir>/hooks/(a hook carrying theovercommitmarker) and left~/.claude/git-hooks/identical — noovercommit-hookin it, noold-hooks/, the globalcore.hooksPathunchanged, and no repository-levelcore.hooksPathleft behind. A commit behind the dispatcher ran the repository’s own.overcommit.yml: itsPreCommitTrailingWhitespacehook rejected a trailing-whitespace commit, and a clean commit went through with no self-update failure. Both controls fail without the adapter, which is why it needs a strategy of its own. A bareovercommit --install(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 a dispatcher that does not export the run-time override dies in overcommit’s own check withNo such file or directory @ rb_file_s_size - ~/.claude/git-hooks/overcommit-hook(installer.rb:59,compare_file).captainhook, v5.29.2 on PHP 8.5.12, Composer 2.11.1 and git 2.54.0 in acomposer:2container, whole-user dispatcher. The globalcore.hooksPathnamed a shared dir withgit-hook-dispatcher.shsymlinked under every hook name; the repository requiredcaptainhook/captainhookthrough Composer, so its binary was only atvendor/bin/captainhook.~/.claude/bin/captainhook installexited 0 through the shim: it found the binary with thevendor/binfallback, installedpre-commit(525 bytes, carrying the CaptainHook marker) and the other hooks in the repository’s own.git/hooks/, left every shared symlink and the globalcore.hooksPathunchanged, and created noold-hooks/. A commit behind the dispatcher ran the repository’s owncaptainhook.json: itspre-commitExistsaction failed on the missing file, printedError: 1 file(s) could not be found, and the commit was rejected. The control fails without the adapter. A barevendor/bin/captainhook installwith the shim removed fromPATHwrote through the shared dispatcher symlinks, sogit-hook-dispatcher.shbecame CaptainHook’s last-installed hook instead of the dispatcher. The next commit did not run the repository’s own hook and died inside the wrong CaptainHook subcommand.
Adding an adapter
- Read the manager’s source for the answers the registry needs — where its install writes and whether it honours, refuses or sets
core.hooksPath; whether aPATHshim can reach the install; the home variable, its default, and whether it expands~; whether a hook run executes code from the home. - Add the adapter to
registry.json. If it needs an install strategy the shim does not implement, add the strategy to the shim and toSTRATEGIESin the generator in the same change. A positional action a manager accepts after global options or in shortened form setsinstall.match: prefix; one Composer installs intovendor/binsetsinstall.vendor_bin. - Give the detection rules: the config file names, whether nested configs count, and a marker every hook it installs contains.
- Deny the default home in
.claude/settings.jsonandtools/sandbox-lint/expected.json, and, if this repository uses the manager, give the sandbox home in theirenv. - Run
tools/dev/sync-hook-managers.py, then the agent-isolation, sandbox-lint and dev tests, and add shim and dispatcher tests for the new strategy. - Add a row above, and update the troubleshooting and verify pages if the manager brings a failure mode of its own.
- Exercise the adapter against the real manager — the install through the shim and a commit behind the dispatcher, plus the control that fails without it — and record the run under Verified runs.
Consumers
setup-isolated-setup-installStep P.3b-whole-user installs the shim once per adapter the operator uses.setup-isolated-setup-verifycheck 19 andsetup-isolated-setup-updatecheck the homes throughsandbox-lint.