diff --git a/.claude/apm-hooks.json b/.claude/apm-hooks.json new file mode 100644 index 0000000..0c96646 --- /dev/null +++ b/.claude/apm-hooks.json @@ -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" + } + ] +} diff --git a/.gitignore b/.gitignore index 1319dad..64a54f4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,11 +40,11 @@ apm_modules/ /.mcp.json # APM hook deployment output — `apm install` copies each package's referenced -# hook scripts here and tracks its own settings.json entries in the sidecar. -# Regenerated on every install; the authoring source is -# plugins//.apm/hooks/ (ADR-0019). +# hook scripts here. Regenerated on every install; the authoring source is +# plugins//.apm/hooks/ (ADR-0019). The .claude/apm-hooks.json ownership +# 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/apm-hooks.json # `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. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7764807..dbdfa77 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,8 +44,9 @@ repos: # alternations went with them: `check-useless-excludes` fails on a # pattern that matches no file. # - # `.claude/settings.json` is the second and last alternation, and it is - # the only one here for a reason other than "generated manifest": + # `.claude/settings.json` and its `.claude/apm-hooks.json` ownership + # 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 audit --ci` replays the install into a scratch tree and diffs # 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 # happened when the SessionStart hook first landed in 2e395a4. # Re-running `apm install` fixes the file; leaving it in scope here - # would re-break it on the very commit that carries the fix. - exclude: '^(\.claude-plugin/marketplace\.json|\.claude/settings\.json)$' + # would re-break it on the very commit that carries the fix. The + # 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 stages: ['pre-commit'] - id: trailing-whitespace diff --git a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md index 7f13d1d..2baab56 100644 --- a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md +++ b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md @@ -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 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 `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,