Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/labels-and-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ or a contract-free mix of substrates (e.g. `tools/spec-inventory` is
| [`tools/skill-reconciler-diff`](../tools/skill-reconciler-diff/) | `substrate:framework-dev` | Deterministic structural diff between two skill trees — parses frontmatter, section headings, step inventory, placeholders, support files, and safety-baseline clauses into a JSON diff object for the `skill-reconciler` skill |
| [`tools/vcs`](../tools/vcs/) | `contract:source-control` | Backend-dispatching implementation of the source-control (VCS) capability ([`tools/github/source-control.md`](../tools/github/source-control.md)); complete Git, Mercurial (Hg), Fossil, and Subversion (SVN) backends |
| [`tools/sourcehut`](../tools/sourcehut/) | `contract:tracker` + `contract:source-control` + `contract:mail-archive` | SourceHut (sr.ht) forge bridge: todo.sr.ht, lists.sr.ht, builds.sr.ht, and git/hg repository reads |
| [`tools/hook-manager`](../tools/hook-manager/) | `contract:hook-manager` | Contract for git-hook managers under the secure agent setup: the install-target and home rules, and the registry of adapters (prek, overcommit and lefthook today) that the hook-manager shim, the whole-user dispatcher and `sandbox-lint` are generated from |
| [`tools/hook-manager`](../tools/hook-manager/) | `contract:hook-manager` | Contract for git-hook managers under the secure agent setup: the install-target and home rules, and the registry of adapters (prek, overcommit, lefthook and captainhook today) that the hook-manager shim, the whole-user dispatcher and `sandbox-lint` are generated from |
| [`tools/tracker`](../tools/tracker/) | `contract:tracker` | Interface spec for the tracker contract's read-only contributor-activity queries (issues filed, triaged and commented on; issues created; first maintainer reply). Pure interface spec; GitHub and Jira implement it, and the rest of the contract is described by `tools/github/tool.md` |
| [`tools/body-field-contract`](../tools/body-field-contract/) | `contract:body-field` | The body-field contract: how every reader and writer finds a tracker body's `### <Name>` fields, with a language-neutral conformance suite run against every implementation. Pure interface spec; implementations: `tools/github-body-field/` (reference), `tools/vetted-ops/`, `tools/local-tracker/`, `tools/jira/` |
| [`tools/security-tracker`](../tools/security-tracker/) | `contract:security-tracker` | Adapter contract for the security tracker the `security-*` skills read and write, selected by `tracker.platform`. Pure interface spec; backends: `tools/github/` (default), `tools/local-tracker/`, `tools/jira/` |
Expand Down
69 changes: 66 additions & 3 deletions docs/setup/sandbox-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,24 @@
- [Root cause](#root-cause-11)
- [Fix](#fix-11)
- [Notes](#notes-11)
- [Reads of `~/.claude/magpie` or `/tmp/claude-<uid>` ask for approval every time](#reads-of-claudemagpie-or-tmpclaude-uid-ask-for-approval-every-time)
- [CaptainHook's Composer plugin replaced the shared hook directory](#captainhooks-composer-plugin-replaced-the-shared-hook-directory)
- [Symptom](#symptom-12)
- [Root cause](#root-cause-12)
- [Fix](#fix-12)
- [Notes](#notes-12)
- [Every Bash call fails with `bwrap: Can't create file … Read-only file system`](#every-bash-call-fails-with-bwrap-cant-create-file--read-only-file-system)
- [Reads of `~/.claude/magpie` or `/tmp/claude-<uid>` ask for approval every time](#reads-of-claudemagpie-or-tmpclaude-uid-ask-for-approval-every-time)
- [Symptom](#symptom-13)
- [Root cause](#root-cause-13)
- [Fix](#fix-13)
- [`git status` shows files you never created, such as `.mcp.json` or `.bash_profile`](#git-status-shows-files-you-never-created-such-as-mcpjson-or-bash_profile)
- [Notes](#notes-13)
- [Every Bash call fails with `bwrap: Can't create file … Read-only file system`](#every-bash-call-fails-with-bwrap-cant-create-file--read-only-file-system)
- [Symptom](#symptom-14)
- [Root cause](#root-cause-14)
- [Fix](#fix-14)
- [`git status` shows files you never created, such as `.mcp.json` or `.bash_profile`](#git-status-shows-files-you-never-created-such-as-mcpjson-or-bash_profile)
- [Symptom](#symptom-15)
- [Root cause](#root-cause-15)
- [Fix](#fix-15)
- [Adding a new entry](#adding-a-new-entry)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -1300,6 +1305,64 @@ The setup installs the shim only for a repository that uses lefthook: a root `le

---

## CaptainHook's Composer plugin replaced the shared hook directory

### Symptom

After a `composer install` or `composer update`, commits stop running the
repository's own hooks, or a repository that never had CaptainHook starts
failing on hooks it did not configure.
`~/.claude/git-hooks/` holds a CaptainHook-generated `pre-commit` and an
`old-hooks/` directory instead of the framework's dispatcher symlinks.

### Root cause

The `captainhook/hook-installer` Composer plugin (or the older
`captainhook/plugin-composer`) runs the install on every dependency update.
It does not go through `~/.claude/bin/captainhook`, so the shim cannot
point it at the repository's own hooks, and it writes into the shared
`core.hooksPath` directory behind the dispatcher.

### Fix

Disable the plugin in the repository's `composer.json`, reinstall the
dispatcher, and run CaptainHook's install through the shim once:

```jsonc
// composer.json
"extra": {
"captainhook": { "disable-plugin": true }
}
```

```bash
# restore the dispatcher symlinks, then install into the repo's own hooks
cd ~/.claude/git-hooks
for h in post-checkout pre-commit prepare-commit-msg commit-msg \
post-commit pre-push post-merge post-rewrite \
pre-rebase pre-merge-commit; do
ln -sf git-hook-dispatcher.sh "$h"
done
~/.claude/bin/captainhook install
```

The shim falls back to `<repository root>/vendor/bin/captainhook` when the
binary is not on `PATH`, so the last command works from anywhere in the
Composer project. A global `captainhook` on `PATH` wins over the project's
pinned one.

### Notes

- An install through the shim warns on stderr when `composer.json` requires
either plugin and neither `extra.captainhook.disable-plugin` nor
`config.allow-plugins` turns it off, so the next update does not undo the
fix silently.
- `setup-isolated-setup-verify` check 8 reports a repository with the
plugin enabled as a warning, and install Step P.3b offers to add
`disable-plugin` before installing the shim.

---

## Reads of `~/.claude/magpie` or `/tmp/claude-<uid>` ask for approval every time

### Symptom
Expand Down
31 changes: 24 additions & 7 deletions docs/setup/secure-agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -1457,17 +1457,33 @@ original argv + inherited stdin, so a failing local `pre-commit` /
`pre-push` still aborts the git operation. A repo with **no** local
hook is a clean no-op.

**The hook-manager PATH shim.** prek and overcommit honour `core.hooksPath`,
so a bare install under whole-user scope would write its hook into the
*shared* dir, not the repo. The framework ships a transparent shim
**The hook-manager PATH shim.** prek, overcommit and CaptainHook honour
`core.hooksPath`, so a bare install under whole-user scope would write its
hook into the *shared* dir, not the repo. The framework ships a transparent
shim
([`tools/agent-isolation/hook-manager-shim.sh`](../../tools/agent-isolation/hook-manager-shim.sh),
installed as `~/.claude/bin/<tool>` with `~/.claude/bin` prepended to
PATH) that rewrites **only** the install subcommand — for prek, injecting
`--git-dir "$(git rev-parse --git-common-dir)"`; for overcommit, setting
`core.hooksPath` to `<absolute common dir>/hooks` — so the hook lands
in the repo-local `.git/hooks/` where the dispatcher finds it.
PATH) that rewrites **only** the install action — for prek, injecting
`--git-dir "$(git rev-parse --git-common-dir)"`; for overcommit and
CaptainHook, setting `core.hooksPath` to `<absolute common dir>/hooks` —
so the hook lands in the repo-local `.git/hooks/` where the dispatcher
finds it.
CaptainHook's Symfony Console CLI also accepts the command after global
options (`captainhook -n install`) or as an abbreviation (`captainhook
inst`), so the shim matches the action at any non-option argument that
prefixes it.
Every other command passes straight through, and the shim is
a no-op on hosts with no global `core.hooksPath`.
Composer keeps CaptainHook in `vendor/bin`, which is not on `PATH`, so
when the shim finds no real binary there it runs
`<repository root>/vendor/bin/captainhook`; a global binary on `PATH` wins
over the project's pinned one, and a `vendor/bin` symlink back to the shim
is refused instead of exec'd.
CaptainHook's `captainhook/hook-installer` and `captainhook/plugin-composer`
Composer plugins still run outside the shim, so its install is one explicit
`~/.claude/bin/captainhook install` with `extra.captainhook.disable-plugin`
set to `true` in `composer.json`; an install through the shim warns when a
plugin is enabled.
When the dispatcher chains into an overcommit hook, it exports the same
`core.hooksPath` override so overcommit's run-time self-check accepts the
hook instead of looking for its hook in the shared dir.
Expand All @@ -1487,6 +1503,7 @@ dispatchers + hook-manager shim):
|---|---|
| prek shim in `.git/hooks/` (installed via the shim) | dispatcher → prek runs that repo's `.pre-commit-config.yaml` |
| overcommit hook in `.git/hooks/` (installed via the shim) | dispatcher → overcommit runs that repo's `.overcommit.yml`; its self-check sees the repo's hooks path |
| CaptainHook hook in `.git/hooks/` (installed via the shim) | dispatcher → CaptainHook runs that repo's `captainhook.json`; the shared dir keeps its dispatcher symlinks |
| local hook installed by a manager requiring no install fix (`simple-git-hooks`, `cargo-husky`, `git-code-format-maven-plugin`, `git-build-hook`) | dispatcher → local hook runs natively (installs straight to `.git/hooks/` ignoring global `core.hooksPath`) |
| hand-written `.git/hooks/<name>`, or one a manager without an adapter put there | dispatcher → that hook runs (heterogeneous tooling coexists) |
| none | dispatcher no-op; git operation proceeds cleanly |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,31 +335,48 @@ for the full rationale + the validated behaviour matrix.
[hook-manager registry](../../../../tools/hook-manager/registry.json)
with an install strategy other than `none`
that a repository the operator works in uses — `~/.claude/bin/prek`,
`~/.claude/bin/overcommit` and `~/.claude/bin/lefthook` today.
`~/.claude/bin/overcommit`, `~/.claude/bin/lefthook` and
`~/.claude/bin/captainhook` today.
A manager whose install strategy is `none` needs no install fix
and gets no shim.
A repository uses a manager when one of its config files is at the root
(or tracked below it, for prek's nested projects) or one of its installed
hooks runs it ([Detection](../../../../tools/hook-manager/README.md#detection));
for prek that is `prek.toml`, `.pre-commit-config.yaml` or
`.pre-commit-config.yml`; for overcommit, `.overcommit.yml`; for lefthook,
its root `lefthook.*`, `.lefthook.*` and `-local` names.
its root `lefthook.*`, `.lefthook.*` and `-local` names; for CaptainHook,
`captainhook.json`.
Install no shim for a manager no repository uses.
The shim takes the tool from the name it is installed under and rewrites only
the install action: for prek it injects
`--git-dir "$(git rev-parse --git-common-dir)"` unless the caller already
passed `--git-dir`;
for overcommit it rewrites `overcommit --install` (or `-i`, `-fi`, `--inst`,
anywhere in argv), pointing `core.hooksPath` at `<absolute common dir>/hooks`;
for CaptainHook it does the same for `install`, which its Symfony Console
CLI also accepts after global options (`captainhook -n install`) or as an
abbreviation (`captainhook inst`);
for lefthook it runs the install with `GIT_CONFIG_GLOBAL=/dev/null` while the
global config holds one, which hides the global `core.hooksPath` from
lefthook's own check and lets its target resolve to the common dir.
Each leaves the install alone when the caller asked for `--help` or is outside
a git work tree, and **every other invocation passes through unchanged.**
When `PATH` holds no real binary, the shim runs
`<repository root>/vendor/bin/<tool>` for an adapter that sets
`install.vendor_bin`, which covers CaptainHook in a Composer project.
It is a no-op on hosts with no global `core.hooksPath`.
A host that installed the older `prek-shim.sh` as `~/.claude/bin/prek` keeps
working; replacing it is optional.

CaptainHook's `captainhook/hook-installer` and
`captainhook/plugin-composer` Composer plugins install on every
`composer install` and `composer update`, outside the shim. Before
installing the shim for a repository whose `composer.json` requires either
plugin, check `extra.captainhook.disable-plugin`: when it is not `true`,
warn the operator that the next dependency update will install into the
shared hooks dir behind the shim, and offer to add
`"captainhook": {"disable-plugin": true}` under `extra`.

3. **Surface the PATH line for the operator to add** to their
`~/.bashrc` / `~/.zshrc` (per the golden rules — never edit the
rc file directly):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ reading this file; read the section for a check whose condition holds.
any other, the remediation is `git config --local --unset
core.hooksPath` in that repository, run by the operator; the
dispatcher still chains to the repository's own `.git/hooks/`.
- ⚠ for a repository that uses CaptainHook
([detection](../../../../tools/hook-manager/README.md#detection)) while
`composer.json` requires `captainhook/hook-installer` or
`captainhook/plugin-composer` without `extra.captainhook.disable-plugin:
true` (or an `allow-plugins` entry that turns it off): the plugin
installs on every `composer install` / `composer update` outside the
shim and replaces the dispatcher in the shared directory. Remediation:
add the flag and re-run `~/.claude/bin/captainhook install`, per
[troubleshooting](../../../../docs/setup/sandbox-troubleshooting.md#captainhooks-composer-plugin-replaced-the-shared-hook-directory).

If `core.hooksPath` is unset (or points elsewhere), the
operator is in **per-project** scope (the default). No further
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
for installs that do not carry the framework source (see `isolated.py`).
"""

FRAMEWORK_FINGERPRINT = "sha256:9879281c90f28d55"
FRAMEWORK_FINGERPRINT = "sha256:ce36f6fbba7ce017"
Loading
Loading