fix(kyberforge): resolve PR #144 review and audit round 1

- factory-audit: no-op hooks, ./ after interpreters, split-quote and
  spaced ${PLUGIN_ROOT} paths, camelCase events in Claude-targeted flat
  files, case-insensitive routing stems, and non-string YAML keys are
  now caught; input: forms and prompt boundary clauses align with
  primitive-author; bats 347 -> 367
- primitive-author: routing forms, quoting guidance, install exit on
  hidden Unicode, argument-hint exception
- forge: drop duplicated gotcha, fit description and body budgets (#143)
- skill-author: primitive-author boundary, Claude-only env vars
- hook: exit unless CLAUDE_PROJECT_DIR is set, so Copilot/Codex never
  run apm update; ADR-0019 correction, ADR-0025 amendment, docs fixes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
2026-09-28 20:13:43 +00:00
parent b2d77b2945
commit df28351d3e
23 changed files with 591 additions and 147 deletions

View File

@@ -24,7 +24,7 @@ The shape Claude Code reads, and therefore the shape to author under `.apm/hooks
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'tool used'" }
{ "type": "command", "command": "echo 'tool used'", "timeout": 10 }
]
}
]
@@ -71,9 +71,12 @@ tracked in a `.claude/apm-hooks.json` sidecar, so an uninstall removes them with
hand-authored hooks. Both `.claude/hooks/` and the sidecar are gitignored install output.
Note that apm's **executable-trust gate is off** unless the consuming project's `apm.yml` has an
`executables:` block — without one, package hooks deploy with no prompt. The allow key is
version-pinned (`kyberforge#<version>`), so a version bump on one side alone stops the hook
deploying; `check-executables-allow-sync` is the pre-push gate that catches it. See ADR-0019.
`executables:` block — without one, package hooks deploy with no prompt. The allow key carries a
version (`kyberforge#<version>`), but apm 0.28.0 matches grants version-blind, so a version bump on
one side does not stop the hook deploying. `check-executables-allow-sync` is a pre-push gate for this
repo's own convention that the key tracks `plugins/kyberforge/apm.yml`'s `version:`, not for an apm
mechanic. See ADR-0019, correction 2026-09-19, and the comment above `executables:` in the root
`apm.yml`.
## The SessionStart hook
@@ -82,14 +85,16 @@ and if anything is behind, runs `apm update --yes` and returns `reloadSkills: tr
session picks up the redeployed content. Rationale, measurements, and the failure modes are in
ADR-0019.
**Where it looks for the lockfile.** The hook resolves a project directory as `${CLAUDE_PROJECT_DIR}`
when the host exports it (Claude Code does, for SessionStart hooks) and the current directory
otherwise, then exits silently unless that directory holds an `apm.lock.yaml` — which is what makes
it inert in any project that does not consume packages through apm. Both `apm` invocations run
against the same resolved directory. The earlier spelling checked a bare `apm.lock.yaml` against the
session's cwd, so a session opened in a subdirectory of an apm-consuming repo no-opped silently.
Keep the cwd fallback: a host that sets no `CLAUDE_PROJECT_DIR` must still get inert-but-harmless
behaviour, not an unset-variable error.
**Claude Code only, and where it looks for the lockfile.** The hook exits 0 at once, silently and
without calling `apm`, unless `CLAUDE_PROJECT_DIR` is set and non-empty — Claude Code exports it for
SessionStart hooks, and that guard is what keeps the hook inert under Copilot and Codex (see below).
It then takes `${CLAUDE_PROJECT_DIR}` as the project directory and exits silently unless that
directory holds an `apm.lock.yaml` — which is what makes it inert in any project that does not
consume packages through apm. Both `apm` invocations run against the same directory. The earlier
spelling checked a bare `apm.lock.yaml` against the session's cwd, so a session opened in a
subdirectory of an apm-consuming repo no-opped silently. Do not reintroduce a cwd fallback: under a
host that sets no `CLAUDE_PROJECT_DIR` the lockfile guard passes in every apm consumer, and the
fallback ran `apm update --yes` there (ADR-0019, correction 2026-09-28).
**The `timeout` in `hooks.json` must exceed the script's own budget.** The script spends at most
`timeout 60 apm outdated` plus `timeout 300 apm update`; the hook entry declares `timeout: 380`, the
@@ -123,10 +128,12 @@ apm 0.28.0):
- **Codex** gets the entry merged into `.codex/hooks.json`, but only when `.codex/` already exists;
otherwise nothing is written.
This is accepted rather than fixed (ADR-0019, amendment 2026-09-28). The hook's behaviour is
Claude-specific anyway — the `startup` matcher, `CLAUDE_PROJECT_DIR`, and the `reloadSkills`
output — and the script exits silently without an `apm.lock.yaml`, so a harness that does run it is
unharmed. The only apm-native way to keep it Claude-only is a separate package whose `apm.yml`
This is accepted rather than fixed (ADR-0019, amendment and correction 2026-09-28). The hook's
behaviour is Claude-specific anyway — the `startup` matcher, `CLAUDE_PROJECT_DIR`, and the
`reloadSkills` output — and a harness that does run it exits immediately, because the script's
first guard exits 0 when `CLAUDE_PROJECT_DIR` is unset. The `apm.lock.yaml` guard cannot do that
job: `apm install` wrote the lock, so it passes in every project the hook reaches. The only
apm-native way to keep it Claude-only is a separate package whose `apm.yml`
declares `target: claude`; per-file target routing (`claude-hooks.json`) is deprecated, and
kyberforge cannot narrow its own `targets:` without dropping its skills from Copilot and Codex.