fix(apm): commit the hook ownership sidecar so fresh installs stay idempotent

apm recognises its own settings.json hook entries only through the
.claude/apm-hooks.json sidecar. With the sidecar gitignored, apm install
in a fresh clone keeps the committed SessionStart entry as user-owned and
appends a duplicate, so apm audit --ci reports drift and the apm-audit-ci
pre-push hook fails. Reproduced on main and this branch with apm 0.28.0.

Commit the sidecar in apm's exact serialisation, exclude it from
pretty-format-json alongside settings.json, and record the correction
in ADR-0019.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 15:26:56 +00:00
parent 55221d099f
commit 3a9d257225
4 changed files with 37 additions and 8 deletions

15
.claude/apm-hooks.json Normal file
View File

@@ -0,0 +1,15 @@
{
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/kyberforge/.apm/hooks/check-apm-current.sh\"",
"timeout": 380
}
],
"_apm_source": "Defame1297/holocron/plugins/kyberforge"
}
]
}

8
.gitignore vendored
View File

@@ -40,11 +40,11 @@ apm_modules/
/.mcp.json /.mcp.json
# APM hook deployment output — `apm install` copies each package's referenced # APM hook deployment output — `apm install` copies each package's referenced
# hook scripts here and tracks its own settings.json entries in the sidecar. # hook scripts here. Regenerated on every install; the authoring source is
# Regenerated on every install; the authoring source is # plugins/<name>/.apm/hooks/ (ADR-0019). The .claude/apm-hooks.json ownership
# plugins/<name>/.apm/hooks/ (ADR-0019). # sidecar is committed, not ignored: without it a fresh clone's install cannot
# claim the committed settings.json entry and duplicates it (ADR-0019).
.claude/hooks/ .claude/hooks/
.claude/apm-hooks.json
# `apm pack` bundle output. The pre-push gate runs pack with --dry-run, so this # `apm pack` bundle output. The pre-push gate runs pack with --dry-run, so this
# only appears after a bare `apm pack` during a release; it is not repo content. # only appears after a bare `apm pack` during a release; it is not repo content.

View File

@@ -44,8 +44,9 @@ repos:
# alternations went with them: `check-useless-excludes` fails on a # alternations went with them: `check-useless-excludes` fails on a
# pattern that matches no file. # pattern that matches no file.
# #
# `.claude/settings.json` is the second and last alternation, and it is # `.claude/settings.json` and its `.claude/apm-hooks.json` ownership
# the only one here for a reason other than "generated manifest": # sidecar are the last two alternations, and they are the only ones
# here for a reason other than "generated manifest":
# apm OWNS that file (ADR-0018, ADR-0019), and # apm OWNS that file (ADR-0018, ADR-0019), and
# `apm audit --ci` replays the install into a scratch tree and diffs # `apm audit --ci` replays the install into a scratch tree and diffs
# the result byte-for-byte. `pretty-format-json` sorts object keys # the result byte-for-byte. `pretty-format-json` sorts object keys
@@ -56,8 +57,11 @@ repos:
# as permanent drift on a file with no git diff -- exactly what # as permanent drift on a file with no git diff -- exactly what
# happened when the SessionStart hook first landed in 2e395a4. # happened when the SessionStart hook first landed in 2e395a4.
# Re-running `apm install` fixes the file; leaving it in scope here # Re-running `apm install` fixes the file; leaving it in scope here
# would re-break it on the very commit that carries the fix. # would re-break it on the very commit that carries the fix. The
exclude: '^(\.claude-plugin/marketplace\.json|\.claude/settings\.json)$' # sidecar is committed so a fresh clone's install can claim the
# settings entry instead of duplicating it (ADR-0019, 2026-09-16
# correction), and it is apm output under the same byte-for-byte replay.
exclude: '^(\.claude-plugin/marketplace\.json|\.claude/(settings|apm-hooks)\.json)$'
- id: check-yaml - id: check-yaml
stages: ['pre-commit'] stages: ['pre-commit']
- id: trailing-whitespace - id: trailing-whitespace

View File

@@ -187,6 +187,16 @@ settings file remains committed, now with apm-generated content in it. ADR-0018'
committed content is exactly `{"hooks": {}}` is superseded on that point only — the rule it was committed content is exactly `{"hooks": {}}` is superseded on that point only — the rule it was
protecting, that nothing repo-authored goes in that file, is unchanged. protecting, that nothing repo-authored goes in that file, is unchanged.
> **Correction (2026-09-16) — the sidecar is committed, not ignored.** apm keeps no ownership marker
> inside `settings.json`; it recognises its own entries by matching them against
> `.claude/apm-hooks.json`, then replaces them. With the sidecar gitignored, a fresh clone holds the
> committed `SessionStart` entry but no sidecar, so `apm install` treats the entry as user-owned,
> keeps it, and adds its own identical copy. `apm audit --ci` then reports `settings.json` drift and
> the `apm-audit-ci` pre-push hook fails. Reproduced on `main` (`a712f2c`) and on this branch with
> apm 0.28.0; committing the sidecar makes the install idempotent and the audit pass. The sidecar
> is apm output like the settings entry it describes, so it is committed for the same reason and
> changes only when the owning package is renamed or moved. `.claude/hooks/` stays ignored.
**Native consumers are protected by a guard, not by the gate.** A host installing holocron through **Native consumers are protected by a guard, not by the gate.** A host installing holocron through
`claude plugin install` auto-discovers `hooks/hooks.json` and does not consult apm's trust gate at `claude plugin install` auto-discovers `hooks/hooks.json` and does not consult apm's trust gate at
all. The script therefore exits silently when there is no `apm.lock.yaml` in the working directory, all. The script therefore exits silently when there is no `apm.lock.yaml` in the working directory,