Development record
A fresh plugin install just works: the hook binary survives install and every update
Press Release
Installing the abcd plugin from its marketplace now yields a working hook surface on the very first session — and keeps working across every plugin update. Today the hooks call
$CLAUDE_PLUGIN_ROOT/abcd, but the plugin root is a git clone of the repo, and the repo (correctly) commits no binary: every fresh install and every update produces a plugin root with noabcdin it, so every hook fails with "No such file or directory" until someone hand-copies a binary in. With this intent, the gap closes itself: a committed POSIX-sh bootstrap runs at session start, and when the plugin-root binary is missing it downloads the latest release binary for the host OS/arch, verifies its SHA-256 against the release checksum manifest, and installs it into the plugin root — refusing on any mismatch. An update never reintroduces the gap, because the bootstrap heals each fresh commit-stamped directory the same way. Nobody clones the repo to use abcd: the binary is all a user needs, and the surface now fetches it itself.
"I installed the plugin exactly as the README says, opened a session in a clean repo, and got two hook errors before I typed anything," said Alice, trying abcd on a second machine. "And when I turned on auto-update so a teammate's fixes would reach me, every update broke it again. Now the first session after install just works, and so does the first session after every update — I never think about the binary at all."
Why This Matters
The distribution story ([[itd-67-installable-versioned-plugin]]) made the repo
its own marketplace, and the README documents both halves: install the
checksum-verified release binary to PATH, and install the plugin from the
marketplace. But the two halves do not meet. hooks/hooks.json invokes
"$CLAUDE_PLUGIN_ROOT/abcd" — a path inside the harness's plugin cache — while
the PATH install puts the binary in /usr/local/bin. Nothing in the repo,
the payload, or the ahoy machinery provisions the plugin-root binary: ahoy install only creates the PATH symlink pointing at $pluginRoot/abcd,
assuming it already exists. A fresh marketplace install therefore fails every
hook (SessionStart, UserPromptSubmit, the Bash guard) in every session,
and the guard failure means shell commands run unguarded — the surface
degrades exactly where it claims to protect.
The update path makes this structural rather than one-off. The harness caches
each installed plugin version in a directory stamped with the source commit
(cache/<marketplace>/abcd/<sha>/). Every update — manual or auto — is a
fresh clone into a fresh directory, which starts with no binary; a hand-copied
fix evaporates on the next update. A maintainer who enables marketplace
auto-update precisely so that merged fixes flow to their machines gets the
opposite: every merge re-breaks the hook surface. There is also a version-skew
corollary: the plugin surface (hooks, commands, agents) tracks the repository
tip, while the newest binary is the last tagged release — fixes that merge
without a release cut can leave the surface ahead of any binary a user can
download ([[itd-73-derived-versioning]], [[itd-69-plugin-metadata-lockstep-update]]).
Decisions (grilled 2026-07-29)
The maintainer resolved the shape questions in a grill session; these are commitments, not options:
- Shape: bootstrap-on-missing. A committed POSIX-sh script under
hooks/(it must need no binary to run — the binary is exactly what is missing) is wired as the firstSessionStarthook. Hook commands keep calling$CLAUDE_PLUGIN_ROOT/abcdunchanged. No PATH fallback in the hook commands. - Version: latest release, always. The bootstrap fetches
releases/latest. No user should ever need the repo — the binary is all they need. Surface-newer-than-binary skew is surfaced, not prevented (prevention belongs to [[itd-73-derived-versioning]]). - Trust bar: same-origin checksums, build-from-source as full trust.
SHA-256 against the release
checksums.txt, refuse on mismatch or a missing manifest entry — identical to the README one-liner's bar. Users wanting a stronger root build from source (go build ./cmd/abcd) and place the binary themselves; the docs say so explicitly. - Guard window: fail open, loudly. While no binary exists (offline first session, checksum refusal, unsupported platform) the Bash guard keeps its current fail-open wrapper: shell commands run, each carrying the UNGUARDED warning. No blocking, no escape-hatch machinery.
- Failure UX: every hook reports, for now. During the no-binary window each hook failure prints the actionable plain-language message. This is a deliberate testing posture; the recorded aspiration is one notice per session with the other hooks silent (tracked as a follow-up, alongside the status-line visibility idea in iss-168).
- Platforms: darwin/linux × amd64/arm64 only. On any other platform the bootstrap says so in plain language and stops; Windows support is its own future intent, not this one.
- PATH: deferred to ahoy. The bootstrap provisions the plugin root only
(no sudo in hooks). Terminal CLI availability keeps its existing owner: the
session-start surface suggests
ahoy installonce, whose existing symlink logic manages/usr/local/bin/abcd.
What's In Scope
- The committed
hooks/bootstrap script: OS/arch detection, download of the latest release binary +checksums.txt, SHA-256 verification (refusing on mismatch or an absent manifest entry), atomic install into$CLAUDE_PLUGIN_ROOT/abcd, and a no-op fast path when the binary is already present and executable. hooks/hooks.jsonwiring that runs the bootstrap before the binary-backedSessionStarthooks.- Update survival: the same bootstrap heals a fresh commit-stamped cache directory after every plugin update with no manual step.
- Failure honesty: while the binary is genuinely unavailable, hooks fail with
the actionable plain-language message (never a raw shell "No such file or
directory"), and
abcd ahoyguard health reports the gap. - Version-skew visibility: when the installed surface (plugin commit) is newer than the newest published binary, the session-start surface says so rather than silently running old-binary/new-surface.
- README plugin section documents the contract: the plugin self-provisions its binary; the PATH install remains for terminal CLI use; build-from-source is the full-trust path.
What's Out of Scope
- The release/versioning pipeline itself — how versions are derived, bumped, tagged, and published stays with [[itd-73-derived-versioning]] and [[itd-69-plugin-metadata-lockstep-update]]; this intent only consumes published release artefacts.
- Marketplace auto-update policy — whether a user enables the harness's auto-update is their choice; this intent makes either answer safe, it does not toggle harness settings.
- Signature infrastructure (minisign/cosign) — the accepted trust bar is same-origin checksums plus the documented build-from-source escape hatch.
- Windows binaries and Windows hook semantics — a future intent.
- The one-notice-per-session failure UX and harness status-line visibility — recorded aspirations (iss-168), follow-up work once the loud posture has been tested.
- Any change to the transport-agnostic core's behaviour — this is a distribution/wiring concern at the plugin surface; the bootstrap is shell, not core.
Acceptance Criteria
Given-When-Then per the itd-1 discipline.
- Given a machine that has never installed abcd, when a user adds the
marketplace, installs the plugin, and starts a session in any repo with
network available, then the bootstrap installs the checksum-verified
latest release binary into the plugin root and every hook (
SessionStart,UserPromptSubmit,PreToolUseguard,PreCompact,SessionEnd) executes successfully — no "No such file or directory", no unguarded-shell warning. - Given a working install, when the plugin updates to a new commit (fresh cache directory), then the first session after the update has a working hook surface with no manual step.
- Given a downloaded binary whose SHA-256 does not match the release
checksums.txt(or a manifest that lacks the platform entry), then nothing is installed, the message names the mismatch, and a corrupted or unpublished artefact never runs. - Given the binary is genuinely unavailable (offline first session,
refused checksum, unsupported platform), when hooks fire, then each
failure prints the actionable plain-language message naming the recovery
step, the Bash guard fails open with its UNGUARDED warning, and
abcd ahoyreports the binary gap in guard health. - Given an unsupported platform, when the bootstrap runs, then it states in plain language that the platform has no released binary and what the supported matrix is — and changes nothing.
- Given an installed surface newer than the newest published binary, when a session starts, then the skew is surfaced to the user rather than silently running a stale binary against newer surface expectations.
- Given a plugin root whose binary is already present and executable, when the bootstrap runs at session start, then it exits without network access — steady-state sessions pay no download and no latency beyond a file test.
Open Questions
None — resolved in the 2026-07-29 grill session; see Decisions. Follow-ups are recorded as scope exclusions and ledger entries (iss-168), not open questions.
Audit Notes
Fidelity review — receipt rcp-bba9a75a2d1d (verifier intent-auditor claude-fable-5-1).
Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:e93232730943fbe0c129bd039144c3d868df37501b9d015c76a6fc7c4662d728 Input attestations: diff:tree at 7c476185 (main lineage, itd-105 shipped)@-;
Acceptance rollup: MET 6 · MET_WITH_CONCERNS 1 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET: hooks/bootstrap.sh resolves releases/latest over https-only curl, fetches the asset and checksums.txt from the same release, verifies SHA-256 and installs atomically; SessionStart runs it first and the UserPromptSubmit, PreToolUse and PreCompact hooks salvage a missing binary before calling it; TestBootstrapInstallsVerifiedBinary, TestBootstrapFreshInstallSelfCheck and TestBinaryHooksProvisionWhenTheBinaryIsAbsent are green at BASE evidence: hooks/bootstrap.sh:568 — "redirect=$(curl -q -fsS --proto '=https' --proto-redir '=https' --max-time 15 -o /dev/null -w '%{redirect_url}' "$releases_url/latest" 2>/dev/null) || redirect=''" evidence: hooks/bootstrap.sh:612 — "curl -q -fsSL --proto '=https' --proto-redir '=https' --max-time 30 -o "$auth_tmp/checksums.txt"" evidence: hooks/hooks.json:13 — ""SessionStart": [" evidence: internal/surface/cli/bootstrap_test.go:408 — "func TestBootstrapInstallsVerifiedBinary(t *testing.T) {" evidence: internal/surface/cli/bootstrap_freshinstall_test.go:103 — "func TestBootstrapFreshInstallSelfCheck(t *testing.T) {" evidence: internal/surface/cli/hooks_selfprovision_test.go:183 — "func TestBinaryHooksProvisionWhenTheBinaryIsAbsent(t *testing.T) {"
- ac-2 — MET: a fresh commit-stamped plugin root is provisioned by the same bootstrap, now from the user-level cache without a download when the cached artefact authenticates; TestBootstrapSecondRootIsProvisionedFromCache and TestBootstrapProvisionsRootFromCacheWithoutDownload model the update and are green evidence: internal/surface/cli/bootstrap_cache_test.go:190 — "func TestBootstrapSecondRootIsProvisionedFromCache(t *testing.T) {" evidence: internal/surface/cli/bootstrap_cache_test.go:103 — "func TestBootstrapProvisionsRootFromCacheWithoutDownload(t *testing.T) {" evidence: internal/surface/cli/hooks_sessionstart_test.go:211 — "func TestSessionStartLeadsWithTheBootstrapSuccess(t *testing.T) {"
- ac-3 — MET: a mismatch and an absent manifest entry each refuse with a message naming the cause and install nothing; TestBootstrapRefusesChecksumMismatch, TestBootstrapRefusesAbsentManifestEntry and TestBootstrapFailsLoudlyAndLeavesNothingHalfInstalled are green evidence: hooks/bootstrap.sh:700 — "refuse "the downloaded $asset does not match its SHA-256 checksum in the release checksums.txt — the artefact is corrupted or is not the published one, so nothing was installed"" evidence: hooks/bootstrap.sh:690 — "refuse "the release checksums.txt lists no entry for $asset, so the download cannot be verified and is not installed"" evidence: internal/surface/cli/bootstrap_test.go:639 — "func TestBootstrapRefusesChecksumMismatch(t *testing.T) {" evidence: internal/surface/cli/bootstrap_test.go:656 — "func TestBootstrapRefusesAbsentManifestEntry(t *testing.T) {" evidence: internal/surface/cli/bootstrap_freshinstall_test.go:179 — "func TestBootstrapFailsLoudlyAndLeavesNothingHalfInstalled(t *testing.T) {"
- ac-4 — MET: every binary-backed hook in hooks.json prints a plain-language message naming the recovery when the binary is missing, the guard hook exits 1 with the UNGUARDED warning, the bootstrap's own refusal names three fixes, and ahoy's guard health reports the missing plugin-root binary; TestBinaryHooksSayWhatIsDegradedWhenUnprovisionable, TestSessionStartReportsAMissingBinaryOnce and TestGuardHealthBinaryUnreachable are green evidence: hooks/hooks.json:30 — "abcd guard: the plugin binary is missing, so shell commands run UNGUARDED until it is provisioned" evidence: hooks/bootstrap.sh:207 — "The abcd binary is not installed in the plugin root, so the abcd hooks cannot run and the shell-hazard guard is inactive" evidence: internal/core/ahoy/guard_health.go:89 — "the plugin-root abcd binary is missing or not executable, so the hook shim fails open on every command" evidence: internal/surface/cli/hooks_selfprovision_test.go:204 — "func TestBinaryHooksSayWhatIsDegradedWhenUnprovisionable(t *testing.T) {" evidence: internal/surface/cli/hooks_sessionstart_test.go:307 — "func TestSessionStartReportsAMissingBinaryOnce(t *testing.T) {" evidence: internal/core/ahoy/guard_health_test.go:95 — "func TestGuardHealthBinaryUnreachable(t *testing.T) {"
- ac-5 — MET: the platform gate maps uname to darwin/linux on amd64/arm64 and any other platform gets a notice naming the platform, the supported matrix and the build-from-source route, changing nothing; TestBootstrapUnsupportedPlatformChangesNothing is green evidence: hooks/bootstrap.sh:426 — "no abcd binary is released for this platform (%s %s). Released binaries cover darwin and linux on amd64 and arm64 only, so nothing was downloaded and nothing was changed." evidence: internal/surface/cli/bootstrap_test.go:710 — "func TestBootstrapUnsupportedPlatformChangesNothing(t *testing.T) {"
- ac-6 — MET_WITH_CONCERNS: hook session-start prints binarySkewNotice when the live plugin root's commit and the cached binary's release commit differ, so the skew is surfaced; the concern is that the notice is deliberately non-directional (it names both directions and claims neither) and stays silent when either commit did not resolve, so the criterion's 'surface newer than binary' is reported as 'at different commits'; TestHookSessionStartReportsBinarySkew and TestHookSessionStartSkewComparesTheLiveRoot are green evidence: internal/surface/cli/cli.go:1628 — "if n := binarySkewNotice(); n != "" {" evidence: internal/surface/cli/skew.go:71 — "the two are at different commits, so the surface may be running ahead of the last release, or the binary ahead of this plugin." evidence: internal/surface/cli/skew.go:56 — "if !resolvedSHA(pluginSHA) || !resolvedSHA(releaseSHA) || pluginSHA == releaseSHA {" evidence: internal/surface/cli/skew_test.go:53 — "func TestHookSessionStartReportsBinarySkew(t *testing.T) {"
- ac-7 — MET: the fast path is a regular-file-and-executable test before any network step, and the hooks skip the salvage entirely when the binary is present; TestBootstrapFastPathTouchesNoNetwork, TestBootstrapFastPathRejectsADirectory and TestBinaryHooksSteadyStateBypassesTheSalvage are green evidence: hooks/bootstrap.sh:302 — "if [ -f "$binary" ] && [ -x "$binary" ]; then" evidence: internal/surface/cli/bootstrap_test.go:381 — "func TestBootstrapFastPathTouchesNoNetwork(t *testing.T) {" evidence: internal/surface/cli/hooks_selfprovision_test.go:251 — "func TestBinaryHooksSteadyStateBypassesTheSalvage(t *testing.T) {"
Gap audit:
- honoured:
- bootstrap-on-missing: a committed POSIX-sh script needing no binary, wired first in SessionStart, hook commands still calling $CLAUDE_PLUGIN_ROOT/abcd evidence: hooks/hooks.json:13 — ""SessionStart": [" evidence: internal/surface/cli/hooks_sessionstart_test.go:185 — "func TestSessionStartIsOneChainedCommand(t *testing.T) {"
- same-origin checksums as the trust bar, curl pinned to https and its own config surface ignored evidence: hooks/bootstrap.sh:151 — "So: -q first on every call below" evidence: internal/surface/cli/bootstrap_test.go:973 — "func TestBootstrapIgnoresCurlsOwnConfigSurface(t *testing.T) {"
- guard window fails open, loudly, with the UNGUARDED warning evidence: internal/surface/cli/hooks_selfprovision_test.go:296 — "func TestGuardHookKeepsItsExitCodeFence(t *testing.T) {"
- build-from-source documented as the full-trust path evidence: docs/how-to/install.md:151 — "For a stronger root of trust than same-origin checksums, build from source —"
- diverged:
- In Scope says the README plugin section documents the self-provisioning contract; the README's plugin section describes the marketplace install and the SHA-256-named plugin archive and defers to the install guide, which is where the bootstrap contract and the build-from-source path are documented
evidence: README.md:109 — "The [install guide] (docs/how-to/install.md) covers building from source and what to do when
abcdisn't found afterwards." evidence: docs/how-to/install.md:45 — "[hooks/bootstrap.sh] (https://github.com/intentdriven/abcd/blob/main/hooks/bootstrap.sh) runs" - the hook commands carry a PATH rung after the plugin root (a recorded-and-owned PATH binary), where the intent's decision says no PATH fallback in the hook commands; SessionStart alone keeps the no-PATH shape evidence: internal/surface/cli/hooks_selfprovision_test.go:475 — "func TestBinaryHooksRunAnOwnedPathBinary(t *testing.T) {" evidence: internal/surface/cli/hooks_selfprovision_test.go:540 — "func TestSessionStartHasNoPathRungAndFailsClosed(t *testing.T) {"
- the skew notice is non-directional by design, where the intent promises to surface a surface-newer-than-binary skew evidence: internal/surface/cli/skew.go:66 — "Non-directional on purpose: comparing two commits establishes that they"
- In Scope says the README plugin section documents the self-provisioning contract; the README's plugin section describes the marketplace install and the SHA-256-named plugin archive and defers to the install guide, which is where the bootstrap contract and the build-from-source path are documented
evidence: README.md:109 — "The [install guide] (docs/how-to/install.md) covers building from source and what to do when
- missing: (none)
Frontmatter
| id | itd-105 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | critical |
.abcd/development/intents/shipped/itd-105-a-fresh-plugin-install-just-works-the.md
open on GitHub ↗ · commit history ↗
References1
- itd-67abcd Is An Installable, Versioned Claude Code Plugin Whose Repo Is Its Own Marketplace And Whose Every Launch Bumps, Tags, And Publishes A New Versionbuilds on
Referenced by11
- adr-38Implicit checks are disk-only — the network answers only an explicit askrelated
- adr-46Persisting the hook binary never weakens the verification posture — every promotion re-verifies, and SessionEnd performs no network workrelated
- adr-2609151706587280The cache reaches PATH only through a home-scoped attestation the environment cannot writerelated
- iss-2610262039378497itd-105 keeps every hook command calling `$CLAUDE_PLUGIN_ROOT/abcd` directly, with the bootstrap as the first SessionStart hook; the configuration chapter says every event's command is `sh` on a per-event shim and SessionStart is one chained commandrelated
- iss-2610262044572933itd-39 plans to extend a Python prompt-router hook; itd-105 says the plugin's hooks call the abcd binaryrelated
- itd-108The plugin installs from the curated release artifact, not the repo, and every cut release reaches users automaticallybuilt on by
- itd-130`abcd update` completes a chosen update in one verbbuilt on by
- itd-132The hook binary survives plugin updatesbuilt on by
- itd-2610161616086564After a plugin update, abcd offers to bring the repository's setup up to datebuilt on by
- itd-2610161617096084After a plugin update, abcd says what changed for this repositorybuilt on by
- spc-21a-fresh-plugin-install-just-works-the-hook-binary-survivesimplemented by
mentions28
- dcn-26080100000000052026-08-01 — itd-105 / spc-21 implementation: the plugin provisions its own binary from a committed POSIX-sh `hooks/bootstrap.sh` wired as the FIRST `SessionStart` hook, per spc-21's Decisions (fast path on an executable binary, darwin/linux × amd64/arm64 gate, atomic `mkdir` lock with a 10-minute stale break, same-origin SHA-256 against the release `checksums.txt`, atomic rename install, `.binary-meta` provenance). Three judgment calls a reviewer should see. (1) The script's TEST-ONLY overrides are TWO env vars, not one: `ABCD_BOOTSTRAP_BASE_URL` (download base, as spc-21 names) plus `ABCD_BOOTSTRAP_API_URL` (release-commit lookup base) — the release-sha resolution hits a different URL shape than the asset download, and folding both behind one variable would have made the fixture server lie about which endpoint was called. Both mirror `ABCD_BIN_TARGET`'s pattern. (2) `release_sha` is read from the commits API (`/commits/<tag>`), NOT the releases API's `target_commitish`: this repo tags annotated (`git tag -a`) and creates the release from an existing tag, so `target_commitish` is the default branch name rather than a commit — a value that would have made the skew notice compare a branch name against a SHA. A value that is not 40 hex is recorded as `unknown` and renders nothing. (3) spc-21's CI-smoke line ("the existing smoke job additionally runs `sh hooks/bootstrap.sh` against a fixture server on both OS runners") is satisfied WITHOUT touching workflow YAML: the script-driving test lives in `internal/surface/cli/bootstrap_test.go`, which `go test ./...` already runs on both legs of ci.yml's ubuntu+macos `check` matrix, and it shells out to the committed script against an `httptest` fixture exactly as the AC describes. A bespoke workflow step would have duplicated that coverage while adding background-process management to CI. Rejected: a PATH fallback in the hook commands (spc-21 forbids it); signature verification (out of scope, same-origin checksums are the accepted bar); guessing `plugin_sha` from `git rev-parse` when the plugin root is not a commit-stamped cache directory (the meta file never guesses).
- dcn-26080100000000062026-08-01 — itd-105 / spc-21, FIX pass after two fresh adversarial pre-PR reviews (one security, one correctness) that converged independently on the same top items. Detector-first throughout; each behavioural fix has a test watched failing before it and passing after. Four claims of the entry above are superseded. (1) `ABCD_BOOTSTRAP_BASE_URL` / `ABCD_BOOTSTRAP_API_URL` were bare `${VAR:-default}` reads, which made them a code-execution primitive rather than a test seam: the binary AND the `checksums.txt` that verifies it are fetched from the same base, so whoever set the variable supplied both the payload and the manifest that "verified" it — verification became vacuous — and the installed binary is then run unattended as the Bash shell guard on every tool call. Both are now honoured for `http://127.0.0.1:*` / `http://localhost:*` only and REFUSED otherwise (refusing beats ignoring: a silently dropped override reads as a passing test), and the production calls pin `--proto =https --proto-redir =https` so no redirect can downgrade the transport or point at `file://`. (2) `release_tag` was scraped from `%{url_effective}` after `-L` on the asset download, which was verified against the real repository to land on `release-assets.githubusercontent.com/<numbers>?<signed query>` — no `/releases/download/<tag>/` segment exists there, so in production the tag was ALWAYS `unknown`, `release_sha` was always `unknown` behind it, and the entire version-skew notice (an intent AC) could never fire. The tag is now resolved BEFORE any download, from `%{redirect_url}` on an unfollowed request to `/releases/latest` (302 -> `/releases/tag/<tag>`), and both the asset and `checksums.txt` are then fetched from the TAG-PINNED path — which also closes the narrower hole that a release cut between the two downloads would have checked a new manifest against an old binary. The fixture was the reason this passed review-blind: it redirected to a conveniently tag-shaped URL, so it now mimics the real CDN shape and serves `checksums.txt` only from the pinned path. Consequence accepted: an unresolvable tag is now a refusal rather than a silent `unknown`, because the alternative is an unpinned pair the checksum step cannot honestly verify. (3) The bootstrap's two messages for a person — the unsupported-platform statement and the one-time `abcd ahoy install` suggestion — went to stdout with exit 0, which on `SessionStart` means model context and nothing the human ever sees; both now go to stderr with exit 2, matching `cli.go`'s existing "non-zero so SessionStart shows it; SessionStart never blocks" convention. This deviates from spc-21's Approach step 2, which specified exit 0 for the unsupported platform: the spec's reason (a reported condition, not a hook fault to retry) is preserved — nothing is changed, nothing is retried — but exit 0 also meant the statement was unreadable by its only audience. (4) `trap cleanup EXIT HUP INT TERM` where `cleanup` returns does not terminate on a signal; POSIX RESUMES the script, verified empirically here, so a SIGTERM mid-run deleted the temp dir and then carried on to report a checksum mismatch that never happened. Now `trap cleanup EXIT` plus `trap 'cleanup; exit 1' HUP INT TERM`, cleanup being idempotent. Smaller, same pass: `.binary-meta` is written through the temp dir and renamed in (a crash mid-write otherwise left a truncated `release_sha` that still parsed) and carries the verified `binary_sha256` so a later check can tell the binary that was verified from one that replaced it; `resolvedSHA` now requires forty lowercase hex rather than merely "not the literal unknown"; the skew notice is non-directional, since comparing two commits establishes that they differ and never which is ahead; the fast path requires a regular file, `[ -x ]` alone being true of a directory too; a run holding the lock sweeps `.bootstrap.tmp.*` orphaned by a SIGKILL (which runs no trap); `hooks/*.sh` is pinned `eol=lf`, a CRLF checkout of this one breaking the script that repairs the hooks rather than merely a hook.
- dcn-26080100000000072026-08-01 — itd-105 / spc-21, hook-ordering assumption DISCLOSED and defensively mitigated rather than verified. spc-21's Approach asserts "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 is an assumption about the harness, and nothing in this repository verifies it; if it is false, a fresh install's first session runs `abcd hook prompt-router-reset` and `abcd hook session-start` before any binary exists, and the intent's AC ("never a raw shell 'No such file or directory'") is missed by the two commands the bootstrap is supposed to protect. A PATH fallback is forbidden by the intent's Decisions and is NOT added. Instead both commands gain an inline existence check in `hooks/hooks.json` — the same shape as the `PreToolUse` guard entry's existing inline wrapper, which turns a bad exit code into a clean message — so if the assumption turns out to be false the failure is at least a plain-language message and a non-blocking exit 2 rather than a raw shell error, with no fallback binary resolution anywhere. This mitigates the SYMPTOM; the assumption itself still wants verifying against the real harness, and the maintainer should treat it as open. In the same file the bootstrap entry gains `"timeout": 240` (seconds): the three curl budgets plus the tag resolution total 180s worst case, which the harness's 60s default would have killed mid-run — which is precisely how the trap bug above and an orphaned temp dir would both have been reached in production. Deferred, deliberately, and bounded: two racers can both proceed when one breaks the other's ten-minute-stale lock, which last-write-wins resolves into a verified binary either way; and the trap is installed just after the lock is taken, leaving a signal window bounded by that same stale-lock recovery. Both are cheaper to state than to fix, and neither can install an unverified binary.
- dcn-26080100000000082026-08-01 — itd-105 / spc-21, THIRD-round fix pass: the environment-driven URL seam in `hooks/bootstrap.sh` is REMOVED rather than patched a third time. Two independent adversarial reviews had already returned BLOCK on two successive allowlist attempts at the same defect — first a bare `${VAR:-default}` read, then a `case http://127.0.0.1:*|http://localhost:*` glob — and both were defeated empirically by fresh reviewers: URL userinfo (`http://127.0.0.1:1@attacker.example`, where everything before the `@` is a username and the real host is the attacker's) walked straight through the glob, and the override path also cleared the `--proto '=https' --proto-redir '=https'` pin, so a redirect could downgrade the transport on top. The pattern was the finding, not the instances: an allowlist over URL syntax is a parser-differential surface, and the thing it guards is a code-execution primitive — the binary AND the `checksums.txt` that verifies it come from the same origin, so whoever names the origin supplies both the payload and the manifest that "verifies" it, and what is installed then runs unattended as the Bash shell guard on every tool call. So `ABCD_BOOTSTRAP_BASE_URL` and `ABCD_BOOTSTRAP_API_URL` are gone; `grep ABCD_BOOTSTRAP hooks/bootstrap.sh` now returns nothing, the script's only environment read at all is `${CLAUDE_PLUGIN_ROOT:-}` (a filesystem destination, never a fetch origin), and the two transport flags are literal on all four curl calls with no variable to clear. This supersedes claim (1) of the fix-pass entry above and the `ABCD_BOOTSTRAP_BASE_URL` mention in spc-21's Testing section, which named a seam that no longer exists. THE TEST SEAM MOVED TO THE TEST SIDE. `internal/surface/cli/bootstrap_test.go` reads the committed script's bytes, replaces the two quoted origin literals with an `httptest` base, writes the result to a temp file, `chmod +x`es it and execs THAT — the shipped bytes are what runs on a user's machine, with no test-only branch anywhere in them. Three properties make the copy trustworthy rather than a fixture that hides the bug (a failure this item already shipped once, when a friendlier fixture concealed that the release tag was always `unknown` in production): the match is the QUOTED, complete assignment value, so a longer origin cannot be prefix-rewritten into a mangled URL; each literal must appear exactly once before and zero times after, or the helper `t.Fatal`s naming the drift; and the happy path additionally asserts the fixture served at least one request, so a substitution that silently missed cannot pass by testing the real GitHub. The fixture speaks TLS (`httptest.NewTLSServer`) because the pin is unconditional, and trust is supplied through `CURL_CA_BUNDLE`/`SSL_CERT_FILE` — curl's own variables, which only ever ADD an issuer curl will accept and can never name a different server, so they are not the class of seam that was removed. Reviewer-facing residue, stated rather than implied: an actor who already controls the hook process's environment can still influence curl through curl's OWN variables (a proxy setting, a CA bundle), which is true of every curl invocation on the machine and is not something a shell script can close — such an actor can also replace `curl` on `PATH`; what is closed is the seam this script itself honoured. Detector-first throughout, each fix reverted in place and watched to fail: the reverted script reproduces the round-2 bypass verbatim (recorded fetch to `http://127.0.0.1:1@abcd-bootstrap.invalid/releases/latest`, no HTTPS pin), and against a live attacker-controlled release server on loopback the pre-fix script took four requests from it and installed its payload while the fixed script took zero and installed the genuine ELF release. Four smaller items in the same pass. (1) `plugin_sha`'s forty-hex gate rests on itd-105's unverified warrant that the harness names each plugin cache directory for its source commit; if that stops holding, every install records `unknown`, the skew notice goes silent permanently, and nothing anywhere says why — so the RAW basename is now recorded as `plugin_root_basename` beside the gated field, never compared and never rendered, purely so the failure is diagnosable from the file rather than only from a comment. Control characters are stripped from it and it is capped at 120 characters, since a directory name may contain a newline (forging a `key=value` line) or enough bytes to push `.binary-meta` past the guarded read and silence the notice by a second route. (2) `mkdir "$lock"` failing was indistinguishable from losing the race and took the same silent `exit 0`; it now refuses loudly when no lock DIRECTORY exists afterwards, because that case is permanent and repeats every session while the race case is transient and correctly quiet. (3) Both message sinks strip control characters from the values they echo, and the skew notice sanitises `release_tag` through `termsafe` like every other untrusted rendered string in this repo — the tag is read out of an HTTP redirect and is the one value in that line with no shape check. (4) `binary_sha256` is recorded and re-checked by nothing; the comment now says so plainly instead of implying an automatic check, because the fast path's whole contract is one file test and adding a SHA-256 recomputation to every session start would trade the AC away for a check nothing yet consumes. Two known asymmetries re-confirmed as disclosed rather than accidental: only the two binary-backed `SessionStart` entries carry the inline existence wrapper — `UserPromptSubmit`, `PreCompact` and `SessionEnd` still invoke the binary bare, which is itd-105's "every hook reports, for now" testing posture and not an oversight; and the unsupported-platform notice's move to exit 2 means it now repeats EVERY session for as long as the platform is unsupported, which is the accepted cost of that same posture (the one-notice-per-session aspiration is iss-168's). Not fixed, noted: the orphan sweep could still delete a live racer's temp directory in the already-accepted >10-minute stale-lock race, and the transcript scanner's `local_username` hard-fails on this script are the same pre-existing false-positive class already firing on `main` for `README.md`.
- dcn-26080100000000092026-08-01 — itd-105 / spc-21, FOURTH round: two fresh independent reviews of the seam-removal fix pass above (one security, one correctness) both returned MERGE, each naming a short list of cheap items rather than a blocker — three rounds of BLOCK was not treated as licence to manufacture a fourth by inflating what these were. All were landed on this branch directly rather than deferred, since none needed a fresh review cycle to verify. (1) The structural regression test guarding the seam removal, `TestBootstrapFetchOriginsAreConstants`, was a DENYLIST — it scanned for `${`, `$ABCD`, `$HTTP`, `$CURL` — so a bypass renamed to any other variable (`$GH_MIRROR` was the reviewer's example) would satisfy it while still redirecting a fetch; reproduced by planting exactly that shape against the committed script and watching the old test pass regardless. Replaced with an ALLOWLIST: every `$NAME`/`${NAME}` reference in the script, after subtracting names the script assigns to itself, must be `CLAUDE_PLUGIN_ROOT` and nothing else — verified this catches the planted `$GH_MIRROR` bypass and passes on the real script. (2) `safe()`'s `tr -d` range stripped ESC and other C0 controls but not CR (`\015`) or DEL (`\177`); a bare CR overprints from column zero, the exact attack class `internal/termsafe`'s own doc comment names, and DEL is the same family. Range widened to `\000-\010\013-\037\177` (keeps only tab and newline, as before); the existing control-character test now plants CR and DEL alongside ESC in the same specimen and watches both survive the pre-fix range before the widened one strips them. (3) `mv -f` onto an existing DIRECTORY at `$binary` moves the downloaded file INTO the directory rather than replacing it — reproduced: the script reported success, exited 2, wrote a truthful-looking `.binary-meta`, and the binary landed at `$binary/abcd-<os>-<arch>` while the hooks stayed broken forever, since the fast path's `[ -f ]` check never becomes true. A guard now refuses before any download when `$binary` exists and is not a regular file; the existing directory-fast-path test was tightened to assert the actual refusal (it had previously accepted a substring of the SUCCESS message as if it proved a refusal happened, which is why the bug had a green test). (4) `plugin_root_basename`'s control-character stripping and 120-byte cap (added in the prior round) had no detector: deleting the sanitisation left the full suite green, and a directory literally named `aa\nrelease_sha=<40 hex>` forged a second `release_sha` line that `skew.go`'s last-wins map parsing would have preferred over the genuine one. Two new cases prove the fix is load-bearing: an embedded-newline specimen asserting `.binary-meta` holds exactly its six declared lines and the genuine value survives, and an oversized-basename specimen asserting the recorded value is exactly 120 bytes. Noted, not landed: the structural allowlist test is still a snapshot of the *shape* the script must hold, not a guarantee against every conceivable future evasion of that shape — it is the cheapest check that catches the two concrete bypasses two independent reviewers actually constructed, not a formal proof.
- dcn-26080100000000102026-08-01 — itd-105 / spc-21, MERGE-GATE fix (concurrent session A): a fresh merge-gate security reviewer found a real gap in that same allowlist test, not in the shipped script. `TestBootstrapFetchOriginsAreConstants` subtracted any name the script "assigns to itself" before checking it against the allowlist, so a SELF-referential seam (`mirror="${mirror:-}"`, then `[ -n "$mirror" ] && repo_url="$mirror"`) read as script-local and the test passed green while a curl shim recorded a fetch to an attacker-named host. Reproduced by planting exactly that shape against the committed script before fixing. The allowlist now taints any name whose own assignment line references itself, on every line where it is assigned, so neither the self-reference nor any later use of the name reads as safe — verified this catches the planted bypass and still passes on the real script. One more merge-gate MINOR landed in the same pass: `mkdir -p "$tmp"` (the temp-download directory) succeeds on a directory that reappears, symlinked, in the window since the preceding `rm -rf`; plain `mkdir` (no `-p`) turns that same-name race into a refusal instead of a curl write through a planted symlink. Both merge-gate reviews (fresh, independent, full-PR-diff) had already returned MERGE before these were found — landed anyway per the merge gate's "fix everything raised" step rather than waiting for a fifth review round, since both are narrow, each has a watched-fail-then-pass test, and neither changes the shipped script's externally observable behaviour except the second (a same-name TOCTOU race becoming a loud refusal, strictly safer).
- dcn-26080100000000112026-08-01 — itd-105 / spc-21, FIFTH round (concurrent session B, landed alongside the entry above): a fresh security review returned BLOCK on a bypass it reproduced, and a fresh correctness review returned MERGE with minor findings; both are closed here on the same branch. (1) BLOCKER — none of `hooks/bootstrap.sh`'s four `curl` calls passed `-q`, so curl loaded its own configuration surface: `$CURL_HOME/.curlrc` (curl >= 7.73, falling back to `$HOME/.curlrc`), where a single `connect-to` or `resolve` line re-points the connection while the URL on the command line still literally reads `https://github.com/…`. The transport pin `--proto '=https' --proto-redir '=https'` therefore still HELD and proved nothing, and because the binary and the `checksums.txt` that verifies it travel the same route, the SHA-256 check was vacuous — the poisoned config supplied both. Reproduced end to end before the fix: a `.curlrc` carrying `connect-to` plus its own `cacert` diverted all five requests to a second local server and installed the forged binary at the guard path, where it would run unattended as the Bash shell guard on every tool call. Fixed by passing `-q` as the FIRST argument of every call (curl honours it in no other position) and by removing, before any fetch, the other names curl reads without being told to: `HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy CURL_HOME` and `CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR`. ACCEPTED COST, deliberately: a machine that can only reach the network through a proxy no longer bootstraps automatically — `refuse()` already names the manual-install and build-from-source ways out, and a proxy plus an attacker-chosen CA is precisely the pairing that makes the redirection succeed on TLS, so the seam is not worth keeping for the convenience. (2) MAJOR — the suite could not have caught (1): it only exercised the removed `ABCD_BOOTSTRAP_*` script variables, never curl's own config surface. `TestBootstrapIgnoresCurlsOwnConfigSurface` now runs the fixture-pointed copy with a poisoned `.curlrc` under both `CURL_HOME` and `HOME`, and asserts the published bytes are installed and the forger is contacted zero times; it was watched failing against the pre-fix script in exactly the shape described above. `TestBootstrapFetchOriginsAreConstants` gained the structural half — every `curl -` line must carry `curl -q ` as well as the literal transport pin, and both scrub lines must be present exactly once — verified by deleting a `-q` and shortening the scrub list and watching it fail. The one test-side concession is that `bootstrapFixtureScript` neutralises the CA-scrub line in its throwaway COPY, because `CURL_CA_BUNDLE` is the only way a real curl trusts the self-signed local fixture; that widens what curl will ACCEPT and can never name a different server, the `CURL_HOME`/proxy scrub stays intact in the copy (which is what makes the poisoned-config case meaningful), and the shipped bytes are held by the structural test. (3) MINOR — the `.binary-meta` install had no regular-file guard, so a directory at that path swallowed the record as `.binary-meta/binary-meta` (the same `mv -f` hazard `$binary` already refuses) while the success notice claimed provenance had been recorded. The install itself is genuine there, so this REPORTS rather than refuses: the notice now says the path exists and is not a regular file, and `TestBootstrapReportsAnObstructedBinaryMeta` proves the record is never moved into the obstruction. NOT FIXED, deliberately: the stale-lock double-break race (already disclosed and accepted above), the "120 bytes" comment wording and the `checksums.txt` text-vs-binary-mode parsing convention (cosmetic, not worth the churn at a merge gate), and the hook-ordering assumption (already disclosed, no code change).
- dcn-26080100000000122026-08-01 — itd-105 / spc-21, coordination note: two independently-running instances of the v0.5.0 loop each reviewed PR #184 at its merge gate concurrently, found different real findings (a self-referential allowlist bypass; a curl config-surface bypass), and pushed both fix commits to `v050/itd-105` minutes apart — rebased together here rather than either overwriting the other. Same collision class the state issue's round-9 comment already disclosed for `v050/iss-159`; recorded again per that precedent, no new action taken beyond the rebase.
- dcn-26080100000000132026-08-01 — itd-105 / spc-21, MERGE-GATE fix (sixth round, test-hardening only): a fresh merge-gate security reviewer reproduced a TWO-HOP bypass of the same fetch-origin allowlist test, and this entry also corrects the claim two entries above. That entry said the allowlist "taints any name whose own assignment line references itself", which is exactly what it did — and exactly why it closed only the ONE-hop case, not "the self-referential bypass class" the fix commit's message implied. Two names that reference each OTHER (`repo_url="${GH_MIRROR:-$gh_default}"` then `GH_MIRROR="$repo_url"`) leave neither line self-referential, so the per-line check passed the planted script green while `GH_MIRROR=https://attacker.invalid/x` genuinely redirected the fetch — the reviewer verified that against the real script logic with a curl-argv-recording shim. `TestBootstrapFetchOriginsAreConstants`'s allowlist is now computed by `bootstrapSafeNames`, which propagates taint to a FIXPOINT over the assignment graph instead of scanning each line once: a name is script-local only if every assignment of it draws on constants, on `CLAUDE_PLUGIN_ROOT`, or on names already safe AT THAT POINT in the file, and the pass repeats until no name is newly tainted. What that closes is chains of self- and mutual reference of ANY length — one hop, the reviewer's two, and the three-hop laundering variant are all detector cases now, alongside the reproduction spliced into the committed script's own bytes. What it does NOT claim is completeness: it is a fixpoint over `$NAME`/`${NAME}` references in `name=` assignments, not a shell parser, so a value laundered through a construct that scan does not model (an `eval`, an eval-time expansion, a name assembled at runtime) is outside what it proves. The reference scan deliberately stays order-INSENSITIVE on the usage side, because a shell function body runs long after the line defining it and "used before assigned" is not a defect there. Second finding in the same pass: the curl scan filtered lines on the substring `curl -`, so any invocation whose first argument is not a flag (`curl "$url" -o /dev/null`) was skipped ENTIRELY — it could be missing `-q` or the transport pin and never be looked at, while the call count stayed consistent with itself because the scan was its own counter. curl is matched as a command WORD now (line head, after a shell operator, command substitution, path prefix, or keyword), with mentions — `command -v curl`, the refusal message's prose, `.curlrc` — held out as non-invocations by their own detector cases. Detector-first on both, each pre-fix behaviour restored in place and watched to fail: the two-hop, three-hop and planted-script cases reported nothing under the old taint rule, and five curl shapes (flags after the URL, no flags at all, an absolute path, after `if`, downstream of a pipe) were invisible to the old substring filter. `hooks/bootstrap.sh` is UNCHANGED — the reviewer's finding is that the regression guard was weaker than its own comment claimed, not that the shipped bytes have a live seam. Left alone deliberately, out of scope for this pass and unfixed: `SSLKEYLOGFILE`/`QLOGDIR` are not scrubbed, the lock and temp `mkdir`s take the ambient umask, and every tool the script runs is PATH-resolved (an actor who can write the PATH can replace `curl` outright, which no shell script closes).
- dcn-26081500000000032026-08-15 — itd-111 planned at the live interview (spc-22 minted, `intent ready` exit 0). The itd-84 decomposition's first live hand-run rendered SPLIT, confirmed: the network trichotomy extracted to adr-38 ("implicit checks are disk-only — the network answers only an explicit ask") + brief invariant 7, which the intent now cites rather than declares; the SessionStart staleness notice vs the iss-206 skew-notice retirement was ruled a SCOPED REPLACEMENT (`refines`, not `reverses`) — steady-state skew machinery stays retired, itd-111 covers dev checkouts and failed provisioning. Interview rulings: explicit check at `abcd version --check` (ahoy stays disk-only); refusal-on-stale deliberately narrow (ahoy install only); unknown vintage (unstamped/dirty builds) is a first-class comparator outcome that fails closed at the refusal gate; harness portability of the session-start channel explicitly deferred to the itd-22 lineage. Independent SOTA fit-challenge UPHELD path 2 with three recorded caveats (seam = version-source provider interface, not a fixed-arity function; git's behind-upstream notice joins the anchors; BuildInfo stamping holes motivate the unknown outcome). AC 6 narrowed to the transition report (the fetch is itd-105/108's); one AC added (unknown vintage). Hand-run graded into the decomposition-calibration note (entry 2).
- dcn-26090800000000012026-09-08 — The hook shims' PATH rung is OWNED-ONLY, and the 2026-08-01 itd-105 / spc-21 entry above ("Rejected: a PATH fallback in the hook commands (spc-21 forbids it)" and "A PATH fallback is forbidden by the intent's Decisions and is NOT added") is superseded by this one (maintainer, ruling option A on iss-2609012039107700 / GHSA-gx3m-3224-qqcv, CWE-426). iss-254 added the rung in 20968bdf (v0.5.1) with no entry reversing spc-21, so the ledger and the tree have disagreed since; what is true from here is that a rung EXISTS and is narrow. A hook takes an `abcd` from PATH only when the lookup yields an absolute path, out of a directory that is neither under the shim's working directory nor world-writable (iss-2609012039117381), AND `~/.abcd/path-entry` records that exact path as this machine's installed binary — a `path=` string comparison, no hashing, because adr-46 rejects hashing ~11 MB on every hook event and this rung fires on every prompt and every tool call. What spc-21 was protecting is what this restores: before the ownership check, a plausible `abcd` earlier on PATH than the operator's became the session's rules loader and, through PreToolUse's pass-through of the guard's 0/1/2 verdict, an approver of every shell command it was asked about. A refused binary now degrades to the shim's existing loud line naming it and the reason; for PreToolUse that is UNGUARDED and exit 1, never the exit 0 the harness reads as approval. SessionStart is unchanged — it has no PATH rung and fails closed. THE CONTRACT CHANGES WITH IT: the install one-liners in README.md and docs/how-to/install.md write `path=` and `binary_sha256=` into `~/.abcd/path-entry` (they know both values; `plugin_root` is optional per readPathEntry), because the documented rescue — "the install one-liner restores it, after which the hooks resolve the PATH binary" — would otherwise stop working the moment the rung became owned-only, and a one-liner run after this change re-points the record at `~/.local/bin` even where `ahoy install --bin-dir` had put the binary elsewhere. Rejected: option B, dropping the rung from PreToolUse alone (smallest change, but it leaves the prompt channel and the transcript path exposed to the same foreign binary); option C, accepting the residual as the operator's own PATH (the same shims do resolve sh, find and printf from PATH, but none of those is handed the guard's verdict, and the advisory is a CWE-426 with a working reproduction). Residual, named rather than papered over: the record is home-scoped and equally writable by any same-UID process, so ownership is provenance against a hijacked PATH, not against a compromised account — the same bound adr-46 decision 4 already states, and the reason the check is called ownership and not verification. Pinned by the four inverted cases in internal/surface/cli/hooks_selfprovision_test.go (owned-runs, unrecorded-refused, record-names-another-file-refused, SessionStart-unchanged), with the PreToolUse exit code asserted as exactly 1.
- iss-168abcd's presence should be visible in the host harness's status line / command line: a managed repo shows an 'abcd-managed' indicator (and possibly guard health) so the user can tell at a glance whether the current session is under abcd management without running a command. Needs a per-host adapter (e.g. a statusline hook) with the usual basics-built-in stance.
- iss-204hooks/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.
- iss-205Every one of the 17 files under commands/ resolves the binary as bare 'abcd' on PATH, with a documented fallback to 'go run ./cmd/abcd'. None of them mentions ${CLAUDE_PLUGIN_ROOT} — grep confirms zero occurrences across commands/. But itd-105 decided 'PATH: deferred to ahoy', so a by-the-book marketplace install provisions the binary ONLY into the plugin root and leaves nothing on PATH. The two halves therefore still do not meet: bootstrap installs a checksum-verified binary that no command can see. Observed on the first manual install (2026-08-10): 'which abcd' reports not found, and bare /abcd fell through to the source-build path — 54s, requiring a Go toolchain, and succeeding only because the marketplace clone happened to be on disk. On a clean machine without Go the entire plugin command surface is non-functional despite a healthy binary sitting in the plugin root. This directly contradicts itd-105's own premise: 'Nobody clones the repo to use abcd: the binary is all a user needs.' The fix is supported and cheap — the plugins reference confirms ${CLAUDE_PLUGIN_ROOT} substitutes in 'Skill and agent content: anywhere the placeholder appears'. Put "${CLAUDE_PLUGIN_ROOT}/abcd" first in the resolution order in all 17 command files, PATH second, 'go run' last.
- iss-206The version-skew notice promised by itd-105 ('Given an installed surface newer than the newest published binary, when a session starts, then the skew is surfaced') can never fire, and fails silently. itd-105's warrant says the harness names each plugin cache directory for the commit it was cloned from; that holds only as a 12-character SHORT sha. Observed .binary-meta from the first manual install (2026-08-10): plugin_root_basename=872f0a1e6aab, which is exactly 'git rev-parse --short=12 HEAD', and plugin_sha=unknown. hooks/bootstrap.sh gates plugin_sha on exactly 40 hex chars, and internal/surface/cli/skew.go resolvedSHA() requires 40 again, so plugin_sha is permanently 'unknown' and binarySkewNotice() returns "" forever on every machine. This is precisely the failure the comment at hooks/bootstrap.sh anticipated and armed a diagnostic for — recording the raw ungated plugin_root_basename beside the gated field so that 'why has the skew notice never fired' has an answer in the file rather than only in a comment. The diagnostic worked exactly as designed; the gate is what is wrong. Fix direction: accept 7-40 hex in both places and compare on the shorter value's prefix length, since one side is a short sha and the other a full one.
- iss-323hook salvage entries invoke bootstrap.sh with no timeout budget below its 180s curl worst case so a slow-link provision is killed and suppressed for ten minutes
- iss-379In-process build-provenance (SLSA attestation) verification in abcd update. Trigger: the trust gap the README concedes is worth closing (e.g. after the public flip). itd-130 ships at the same-origin checksums bar (itd-105); the release workflow already attests binaries and checksums.txt, so abcd update could verify the attestation before the swap for a stronger root of trust than same-origin checksums — at the cost of a heavy dependency (sigstore-go). Twin of itd-108's deferred offline signing key (minisign): both close the 'the forge is the identity root' gap that an attestation alone does not.
- iss-2608210934566221Relocate the hook binary and .binary-meta from the plugin cache dir to the harness persistent data dir (CLAUDE_PLUGIN_DATA, ~/.claude/plugins/data/abcd-abcd-marketplace/). The harness re-clones every update into a fresh commit-stamped cache dir and its docs state extra files there are NOT preserved across updates, so the ~11MB binary is re-fetched on every plugin update by design. The data dir survives updates and is deleted only on full uninstall. Fixes in one move: the per-update re-download, the SessionEnd bootstrap-cancellation window, per-version binary disk copies (orphaned cache dirs are GC'd ~14 days after .orphaned_at), and the PATH-symlink rot captured separately. Reworks the itd-105/spc-21 bootstrap fast path and every pluginBinaryPath consumer.
- iss-2609012039102770GHSA-4q78-ccfv-f374 (CWE-345, advisory severity low): the owned PATH-copy verification is self-referential under an env-supplied CLAUDE_PLUGIN_DATA. `internal/core/ahoy/data_dir.go:pluginDataDir` is `os.Getenv("CLAUDE_PLUGIN_DATA")` and nothing else; `owned_copy.go:cacheRecordedSHA` reads `binary_sha256` from a `binary-meta` beside the artefact under that same directory; `apply.go:installOwnedEntry` hashes the artefact, compares it to that record, writes the bytes 0755 as the owned PATH entry and records provenance in `~/.abcd/path-entry`. The bootstrap's manifest authentication (`hooks/bootstrap.sh`, cache_trust=manifest, iss-2608210934566228) covers only the cache-to-plugin-root promotion; the cache-to-PATH promotion re-verifies against the co-located record only, which an attacker who writes both satisfies trivially — adr-46 decision 3 states exactly this principle for the bootstrap. Reproduced at v0.7.0: with CLAUDE_PLUGIN_DATA and ABCD_PLUGIN_ROOT pointed at attacker-chosen directories, `ahoy install --yes --adopt` installed a one-byte file as `~/.local/bin/abcd`, recorded its hash and the fake plugin root in path-entry, and `classifyBinTarget` then vouched for it as an owned copy.
- iss-2609012039107700GHSA-gx3m-3224-qqcv (CWE-426, advisory severity medium): the hook shims' PATH fallback executes an unverified `abcd` as the guard and rules loader. In `hooks/hooks.json`, UserPromptSubmit, PreToolUse and PreCompact carry `if [ -z "$g" ] && command -v abcd; then g=abcd; fi` and SessionEnd `exec abcd hook session-end` when the plugin-root binary is missing; PreToolUse's fence passes exit 0/1/2 through, so a foreign binary exiting 0 approves every shell command. SessionStart has no PATH rung and fails closed. Reproduced at v0.7.0 with the shipped PreToolUse command, a failing bootstrap and a hijack directory first on PATH: the planted binary received `guard hook` plus the tool input and its exit 0 was accepted as armed. The rung was introduced by iss-254 (20968bdf, v0.5.1), is pinned as desired behaviour by `internal/surface/cli/hooks_selfprovision_test.go:TestBinaryHooksFallBackToAPathBinary`, and is a documented contract (docs/how-to/install.md: "the plugin root first, then an abcd on PATH … the install one-liner restores it"; brief 04-surfaces/01-ahoy.md section 9; brief 05-internals/03-configuration.md). The record contradicts itself: the 2026-08-01 decisions [dcn-2608010000000005](../../decisions/dcn-2608010000000005-itd-105-spc-21-implementation-the-plugin.md) and [dcn-2608010000000007](../../decisions/dcn-2608010000000007-itd-105-spc-21-hook-ordering-assumption.md) (itd-105 / spc-21) state "Rejected: a PATH fallback in the hook commands (spc-21 forbids it)" and "A PATH fallback is forbidden by the intent's Decisions and is NOT added"; iss-254 later added it with no DECISIONS entry reversing that.
- iss-2609290448510918The predecessor-store qualifier sweep reaches past the six intents iss-2609261536147903 named: outside drafts/ and those six, 192 citation sites across about forty intents name a spc-N at or below spc-70 that is not the citing intent's own spec, with no '(predecessor store)' on the line. Some are live cross-references (the cold-reading family's intents cite each other's live specs), and some are the predecessor store's (itd-4, itd-6, itd-29, itd-47 and itd-49 describe pre-rebuild work in its terms), so each site needs the same reading against the live spec it collides with; the specs charter's Two spc-N Namespaces section is the rule. Found while sweeping the pattern of iss-2609261536147903 in lane drainDrift3; a census script over intents/{planned,shipped,disciplines} reproduces the count.
- itd-1Every Intent Has Acceptance Criteria; Every Audit Verifies Them
- itd-69Plugin Metadata Stays Consistent Across Every Duplicated Surface
- itd-73abcd Cuts the Version — You Never Type One
- itd-109Acceptance Criteria Verify Themselves, And The Manual Rest Renders For A Human
- itd-111A Stale abcd Never Answers Silently
- spc-22a-stale-abcd-never-answers-silently-every-surface-that-runs
- spc-32`abcd update` — fetch, verify, swap, in one verb