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
6 changes: 4 additions & 2 deletions .abcd/development/brief/04-surfaces/01-ahoy.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,10 @@ Steps, run in parallel where independent:
`hooks/hooks.json` is present in the plugin install AND contains the three
required event entries (`UserPromptSubmit`, `SessionStart`, `PreCompact`)
each referencing the expected prompt-router hook commands. The shipped
manifest also wires `abcd hook session-start` (a second `SessionStart`
command), `abcd hook session-end` (a `SessionEnd` event), and `abcd guard
manifest also wires `abcd hook session-start` (chained after the bootstrap
and ahead of `prompt-router-reset` inside the ONE `SessionStart` command —
the harness runs sibling hooks in parallel, so the event carries a single
entry), `abcd hook session-end` (a `SessionEnd` event), and `abcd guard
hook` (a `PreToolUse` event, matcher `Bash`, that checks a shell command
against the hazard registry before it runs) — five event types in all;
verification covers only the three prompt-router commands above. A missing or
Expand Down
4 changes: 3 additions & 1 deletion .abcd/development/brief/05-internals/03-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ abcd/
│ ├── release-changelog-composer.md / ruthless-reviewer.md / security-reviewer.md
│ └── sota-researcher.md # plus per-agent fixtures/ dirs, README.md, CHANGELOG.md
└── hooks/ # Claude Code event hooks — each command shells directly to the binary
└── hooks.json # UserPromptSubmit → hook prompt-router; SessionStart → prompt-router-reset + session-start;
└── hooks.json # UserPromptSubmit → hook prompt-router; SessionStart → ONE chained command:
# bootstrap.sh, then session-start + prompt-router-reset, each fed a copy of the
# payload (siblings would run in parallel and share one stdin);
# PreToolUse (matcher Bash) → guard hook; PreCompact → prompt-router-reset; SessionEnd → session-end
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ plus wiring plus reporting: no `internal/core` behaviour changes.

- **`hooks/bootstrap.sh`** (new, committed): the self-provisioning script.
Needs no abcd binary to run — the binary is exactly what is missing.
- **`hooks/hooks.json`**: the bootstrap becomes the first `SessionStart`
entry, ahead of the binary-backed `prompt-router-reset` / `session-start`
commands.
- **`hooks/hooks.json`**: the `SessionStart` event is ONE entry whose command
runs the bootstrap and then the binary-backed `prompt-router-reset` /
`session-start` calls in the same shell (see Hook wiring, and the correction
recorded there).
- **`$CLAUDE_PLUGIN_ROOT/.binary-meta`** (new, written by the bootstrap): one
small key=value file recording `release_tag`, `release_sha` (when
resolvable), `fetched_at`, and `plugin_sha` — the input for skew reporting.
Expand Down Expand Up @@ -85,15 +86,41 @@ Behaviour, in order:

### Hook wiring

`hooks.json` gains, as the first `SessionStart` hook:
`hooks.json` declares ONE `SessionStart` entry, whose command runs the
bootstrap and then the two binary-backed calls in a single shell:

```json
{"type": "command", "command": "\"$CLAUDE_PLUGIN_ROOT/hooks/bootstrap.sh\""}
{"type": "command", "timeout": 240, "command": "… bootstrap.sh …; i=$(cat); … abcd hook session-start; … abcd hook prompt-router-reset"}
```

Ordering within one event's hook list is preserved by the harness, so the
binary-backed session hooks run after the bootstrap in the same event. The
`UserPromptSubmit`/`PreToolUse`/`PreCompact`/`SessionEnd` commands are
Two properties of that chain are load-bearing and easy to lose. **The payload is
read once and piped to each call separately.** Every hook verb takes its input
with `io.ReadAll` over the whole of stdin (`readHookInput`), so two calls sharing
one stdin leave the second reading EOF — `hook session-start` would fail to
unmarshal and take its silent `return nil` path, disabling both of its notices
in every session. **`session-start` runs before `prompt-router-reset`**, because
the reset ends with an unconditional "abcd rules: reset session" diagnostic on
stderr, and whichever call runs first owns the only line the transcript renders.
The exit precedence (bootstrap, then `session-start`, then the reset) is
computed from saved status codes and does not depend on that order.

**Correction (2026-08-12, iss-204 / iss-208).** This section wired the
bootstrap as the first of THREE sibling `SessionStart` entries on the warrant
that *"ordering within one event's hook list is preserved by the harness, so
the binary-backed session hooks run after the bootstrap in the same event"*.
That warrant is **false**, and it was load-bearing: the harness runs every hook
matching an event **in parallel** (the hooks reference says so verbatim). Both
gated entries raced the ~10.7 MB download, lost, printed "the plugin binary is
not installed", and genuinely did not run — on every fresh install and every
plugin update, since an update lands in a fresh commit-stamped cache directory
with no binary. The three entries are collapsed into the one command above, so
the sequencing is OWNED by the manifest rather than assumed of the harness; the
two gated messages become one; and the bootstrap's own message is emitted
first, so the single line the transcript renders is the success rather than a
missing-binary complaint (iss-208). Nothing else in this spec rests on hook
ordering. Do not restore the ordering warrant.

The `UserPromptSubmit`/`PreToolUse`/`PreCompact`/`SessionEnd` commands are
unchanged — on the first-ever event before any `SessionStart` completed they
fail as today (fail-open guard with UNGUARDED warning, per the intent's
guard-window decision).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ category: "bug"
source: "user-observation"
found_during: "first manual plugin install test (2026-08-10)"
found_at: "hooks/hooks.json"
resolution: "Collapsed the three SessionStart entries into one chained command (bootstrap, then prompt-router-reset and session-start in the same shell), so sequencing is owned by hooks.json rather than assumed of a harness that runs sibling hooks in parallel. spc-21's false ordering warrant corrected in place."
impact: fix
---

hooks/hooks.json puts bootstrap.sh and the two binary-backed SessionStart hooks in ONE event group and relies on list order, but the harness runs all matching hooks in PARALLEL — the Claude Code hooks reference states verbatim 'All matching hooks run in parallel.' spc-21 asserts the opposite: 'Ordering within one event's hook list is preserved by the harness, so the binary-backed session hooks run after the bootstrap in the same event.' That warrant is false, and it is load-bearing. On a fresh install the two gated hooks evaluate [ -f "$CLAUDE_PLUGIN_ROOT/abcd" ] while bootstrap.sh is still downloading the ~10.7MB release binary (timeout 240), lose the race, and both print 'the plugin binary is not installed'. Observed on the first manual marketplace install (2026-08-10): both errors printed BEFORE bootstrap's own success notice, which is direct evidence of parallel execution. Consequence is not only noise — 'hook prompt-router-reset' and 'hook session-start' genuinely do not run for that session, and itd-105 AC#1 ('every hook executes successfully — no No such file or directory, no unguarded-shell warning') fails on every fresh install AND every plugin update, since each update lands in a fresh commit-stamped cache directory with no binary. Fix direction: collapse the three entries into ONE SessionStart command that runs bootstrap.sh and then the two binary calls in a single shell, so the sequencing is owned here rather than assumed of the harness.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ source: "user-observation"
found_during: "first manual plugin install test (2026-08-10)"
found_at: "hooks/bootstrap.sh"
blocked_by: [iss-204]
resolution: "The chained SessionStart command emits the bootstrap's own message first, so the single line the transcript renders on a fresh install is the checksum-verified success rather than a missing-binary complaint. The non-zero exit stays: it is the only channel that puts stderr in front of the human."
impact: fix
---

A successful bootstrap install is rendered to the user as a third 'SessionStart:startup hook error', indistinguishable from the two genuine failures above it. hooks/bootstrap.sh's notice() exits 2 deliberately, and its comment gives the sound reason: a SessionStart hook's stdout becomes model context, while only a non-zero exit puts stderr in front of the human. But the harness renders ANY non-zero SessionStart exit as a '<hook name> hook error' notice — the docs confirm exit 2 'renders in the transcript as a <hook name> hook error notice, the same way a non-blocking error does'. So there is no 'notice' channel distinct from 'error', and the checksum-verified happy path is labelled a fault. Observed on the first manual install (2026-08-10): three consecutive hook-error lines, of which the third was the install SUCCEEDING. Fix direction: fold this into the single chained SessionStart entry proposed in iss-204 and lead the visible first line with the success, since the transcript shows only the first line of stderr; the honest-failure posture for the genuinely-no-binary window is unaffected.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,34 @@ called out in a **Breaking** section.

### Fixed

- **The session-start hooks run after the bootstrap that provisions their binary,
and a successful install reads as success** (iss-204, iss-208). The hook
manifest listed the bootstrap and the two binary-backed commands as three
sibling `SessionStart` entries and relied on list order; the harness runs every
hook matching an event in parallel, so both gated entries raced a ~10.7 MB
download, lost, printed "the plugin binary is not installed", and genuinely did
not run — on every fresh install and every plugin update, since an update lands
in a fresh cache directory with no binary. The three entries are now ONE
command that runs the bootstrap and then both binary calls in a single shell,
so the sequencing is owned by the manifest rather than assumed of the harness.
Chaining them makes two further properties load-bearing, and both are held
explicitly: the hook payload is read once and piped to each call separately,
because every hook verb consumes the whole of stdin and a shared stdin would
leave `session-start` reading EOF and silently disabling its notices; and
`session-start` runs ahead of `prompt-router-reset`, whose unconditional
success diagnostic would otherwise be the one line the transcript renders.
The bootstrap's own message is emitted first, which is what the transcript
renders: on a fresh install the visible line is the checksum-verified success
rather than one of two missing-binary complaints, and the two complaints
collapse into one. The honest-failure posture is unchanged — a refusal keeps
its message and its exit code, a binary that is genuinely absent is still said
out loud, and the binary calls' stdout still reaches the model untouched. The
spec that shipped the bootstrap carried the false warrant ("ordering within one
event's hook list is preserved by the harness") as a load-bearing claim; it is
corrected in place, with the brief's two descriptions of the manifest. Parallel
hook execution and the plugin cache are not present in CI, so the end-to-end
proof is the manual install gate; what CI holds is the manifest's shape and the
chained command's behaviour against fixtures.
- **`ahoy install` prompts read a piped answer, in a fixed order, and `--yes` says
what it does not cover** (iss-167, iss-166). The prompter attached to stdin only
when stdin was a terminal, so `yes | abcd ahoy install` — the first thing an
Expand Down
4 changes: 1 addition & 3 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
],
"SessionStart": [
{"hooks": [
{"type": "command", "timeout": 240, "command": "\"$CLAUDE_PLUGIN_ROOT/hooks/bootstrap.sh\""},
{"type": "command", "command": "if [ -f \"$CLAUDE_PLUGIN_ROOT/abcd\" ] && [ -x \"$CLAUDE_PLUGIN_ROOT/abcd\" ]; then \"$CLAUDE_PLUGIN_ROOT/abcd\" hook prompt-router-reset; else echo \"abcd: the plugin binary is not installed, so the rule loader did not reset for this session — hooks/bootstrap.sh provisions it at session start; start a session with network access, or install per https://github.com/REPPL/abcd-cli#install\" >&2; exit 2; fi"},
{"type": "command", "command": "if [ -f \"$CLAUDE_PLUGIN_ROOT/abcd\" ] && [ -x \"$CLAUDE_PLUGIN_ROOT/abcd\" ]; then \"$CLAUDE_PLUGIN_ROOT/abcd\" hook session-start; else echo \"abcd: the plugin binary is not installed, so the session-start checks did not run — hooks/bootstrap.sh provisions it at session start; start a session with network access, or install per https://github.com/REPPL/abcd-cli#install\" >&2; exit 2; fi"}
{"type": "command", "timeout": 240, "command": "[ -n \"${CLAUDE_PLUGIN_ROOT:-}\" ] || exit 0; b=''; s=0; if [ -x \"$CLAUDE_PLUGIN_ROOT/hooks/bootstrap.sh\" ]; then b=$(\"$CLAUDE_PLUGIN_ROOT/hooks/bootstrap.sh\" 2>&1 >/dev/null </dev/null); s=$?; fi; [ -z \"$b\" ] || printf '%s\\n' \"$b\" >&2; if [ -f \"$CLAUDE_PLUGIN_ROOT/abcd\" ] && [ -x \"$CLAUDE_PLUGIN_ROOT/abcd\" ]; then i=$(cat); printf '%s' \"$i\" | \"$CLAUDE_PLUGIN_ROOT/abcd\" hook session-start; e=$?; printf '%s' \"$i\" | \"$CLAUDE_PLUGIN_ROOT/abcd\" hook prompt-router-reset; p=$?; [ \"$s\" -eq 0 ] || exit \"$s\"; [ \"$e\" -eq 0 ] || exit \"$e\"; exit \"$p\"; fi; [ -n \"$b\" ] || printf '%s\\n' \"abcd: the plugin binary is not installed, so the rule loader did not reset and the session-start checks did not run — hooks/bootstrap.sh provisions it at session start; start a session with network access, or install per https://github.com/REPPL/abcd-cli#install\" >&2; [ \"$s\" -eq 0 ] || exit \"$s\"; exit 2"}
]}
],
"PreToolUse": [
Expand Down
Loading