fix(pre-commit): keep key order in pretty-format-json so apm-owned JSON survives

pretty-format-json sorts object keys by default, but apm emits insertion
order and `apm audit --ci` diffs its output byte-for-byte. A file in the
formatter's scope therefore drifts on every commit with an empty git diff.

Pass --no-sort-keys so `.claude/settings.json` and `.claude/apm-hooks.json`
round-trip untouched and drop them from the exclude. marketplace.json stays
excluded: it carries literal em dashes the formatter re-escapes to —.

tests/test-pretty-json-tool-owned.sh runs the repo's real autofix hooks over
copies of the four tracked apm-owned files and fails if any is rewritten, so
losing the flag fails a test instead of surfacing as drift.

Refs: #102

Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
Defame1297andClaude Code committed 2026-09-30 16:02:21 +00:00
1 parent 3ff0741857
commit c2c56ff948
4 files changed
+156 -50

No files matched your search

+19 -15
View File
@@ -1279,24 +1279,28 @@ point only). Machine-specific settings go in the gitignored `.claude/settings.lo
does not deploy and the replay does not compare; shared enforcement belongs in
`.pre-commit-config.yaml`.
### Why it is excluded from `pretty-format-json`
It is in the **second and last alternation** in that hook's `exclude:` pattern, and that alternation
is the only one there for a reason other than "generated manifest". Mind which number you are
quoting: the pattern is `^(\.claude-plugin/marketplace\.json|\.claude/(settings|apm-hooks)\.json)$`
— **two top-level alternations, expanding to three real tracked files**:
`.claude-plugin/marketplace.json`, this one, and its committed `.claude/apm-hooks.json` sidecar,
which is apm output under the same byte-for-byte replay and is excluded for the same reason.
### Why `pretty-format-json` runs with `--no-sort-keys`
`pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed, while apm's hook
integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). Leaving the
file in that hook's scope therefore rewrites apm's output into a form apm would never produce on the
way into **every** commit, and `apm-audit-ci` then reports permanent drift on a file with an empty
`git diff` — exactly what happened when the `SessionStart` hook first landed in `2e395a4`. Re-running
`apm install` fixes the file; leaving it in scope would re-break it on the very commit carrying the
fix.
integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). In scope with
the default, the formatter rewrites apm's output into a form apm would never produce on the way into
**every** commit, and `apm-audit-ci` then reports permanent drift on a file with an empty `git diff`
— exactly what happened when the `SessionStart` hook first landed in `2e395a4` (#102).
**Load-bearing. Do not tidy it out of that list** (see `LESSONS.md`, 2026-08-14).
The hook now passes `--no-sort-keys`, so this file and its committed `.claude/apm-hooks.json` sidecar
(apm output under the same byte-for-byte replay) need **no exclude**: the formatter's default 2-space
indent already matches apm's, and with insertion order kept they round-trip untouched.
`tests/test-pretty-json-tool-owned.sh` runs the repo's real `pretty-format-json`, `end-of-file-fixer`
and `trailing-whitespace` over copies of the four tracked apm-owned files (these two,
`.claude-plugin/marketplace.json` and `apm.lock.yaml`) and fails if any is rewritten, so dropping
`--no-sort-keys` — or an apm change that makes another autofixer touch its output — fails a test
instead of surfacing as drift on an unchanged file.
**Load-bearing. Do not remove `--no-sort-keys`.** `.claude-plugin/marketplace.json` is the one path
still in that hook's `exclude:`: it carries literal em dashes that the formatter re-escapes to
`\u2014`, which `--no-ensure-ascii` would stop but for every JSON file. This closes the JSON case
only; a new tool-owned file in the scope of another autofixer is still caught only by that test,
not by a derived gate.
## Pushing without a network