Development record
Implicit checks are disk-only — the network answers only an explicit ask
Context
itd-111 (staleness detection) declared a "network trichotomy" as one of its
design decisions: implicit checks read disk, the network answers only an
explicit --check, and provisioning fetches only pinned artifacts. The
itd-84 decomposition at that intent's planning interview (2026-08-15) found
the rule is not a feature decision at all — "no version-discovery request
exists anywhere in abcd" binds the whole system, and it would bind it even if
itd-111 were never built. A trust rule wearing a design-decision coat is the
monolith failure itd-84 exists to catch, so the rule was extracted here.
The posture it protects is already load-bearing: abcd positions itself as a tool whose record stays on the machine, and this repo's own SOTA notes score competing tools down for ambient network traffic. The update-notifier / Homebrew pattern — a background version-discovery request on ordinary use — fails that bar even when its UX grammar (cached comparison, gentle nudge, one-command fix) is worth keeping. The pattern's own ecosystems offer no disk-only mode, only full opt-out (verified at the itd-111 fit-challenge, 2026-08-15).
Decision
Three tiers, exhaustive — every network touch in abcd falls into exactly one:
- Implicit operations never touch the network. Anything abcd does without the user naming a network action — session-start checks, status renders, audits, ordinary verbs — reads only what is on disk (embedded build info, checkout state, plugin-cache manifests, committed config). No version-discovery request exists anywhere in abcd.
- The network answers only an explicit ask. A fetch happens when the
user invokes a verb or flag whose documented meaning is that fetch —
abcd version --check,abcd docs cite refresh. The output names its source. - Provisioning completes a chosen update, it never discovers one. When a plugin update names a new pinned binary version, bootstrap fetches exactly that checksum-verified artifact from the named release — the user's update action was the ask; the pin and checksum bound what may arrive.
Spelling note (2026-09-30). Tier 2's first example is spelt
abcd update --check:abcd version --checkmoved there (itd-2609212130136102) and the old spelling is removed in v0.12.0 (BT1, iss-2609251324599468), so it is an unknown command. The rule is unchanged.
Consequences
- The zero-network test harness the citation gate already uses is the enforcement seam: tier-1 paths must pass under it, and a new verb that fetches implicitly is a defect, not a preference.
- Update-notification UX is built over disk sources only (itd-111's comparison provider), and "check for updates" is forever a verb the user runs, not a thing abcd does.
- Any future adapter that wants ambient network behaviour (telemetry, auto update discovery) requires superseding this ADR in the open — it cannot ride in as an implementation detail.
Frontmatter
| id | adr-38 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-08-15 |
.abcd/development/decisions/adrs/0038-implicit-checks-are-disk-only.md
open on GitHub ↗ · commit history ↗
References5
- adr-22Bundled dependencies become pluggable adapters over a native defaultrelated
- adr-23A transport-agnostic Go core behind thin front doorsrelated
- itd-105A fresh plugin install just works: the hook binary survives install and every updaterelated
- itd-108The plugin installs from the curated release artifact, not the repo, and every cut release reaches users automaticallyrelated
- itd-111A Stale abcd Never Answers Silentlyrelated
Referenced by11
- adr-46Persisting the hook binary never weakens the verification posture — every promotion re-verifies, and SessionEnd performs no network workrelated
- adr-48The website deploys on release, not on mergerelated
- adr-52The semantic gate sits on the wrong side of the tagrelated
- adr-2609151706587280The cache reaches PATH only through a home-scoped attestation the environment cannot writerelated
- adr-2610031352212697Claims are branches on the shared remote; nothing kept on one computer is the register; claim text is datarelated
- adr-2610032150581128abcd is reachable on a network only through the dashboard, only while the person runs it, and only for people the person lets inrelated
- adr-2610150729333337A status branch moves without a preflight receiptrelated
- adr-2610181003541997The away mode's stop check reads the forge once, as its documented meaningrelated
- adr-2610241022541063Each person's commit identity lives on their own machine, not in the repositoryrelated
- adr-2610241057124151abcd installs the tools it needs from downloads pinned in its own releaserelated
- adr-2610241101463288Setup asks short questions and lists its defaults on one planrelated
mentions28
- 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-26091500000000102026-09-15 — GHSA-4q78-ccfv-f374 (iss-2609012039102770) is closed by OPTION B, ruled by the maintainer at an interactive question: bind the cache to a record the environment does not choose alone. The owned PATH-copy promotion re-verified the cache only against the `binary-meta` beside it, and `CLAUDE_PLUGIN_DATA` is taken from the environment as given, so whoever chose the directory wrote both the bytes and the record that "verified" them — reproduced at v0.7.0 as a one-byte file installed 0755 as `~/.local/bin/abcd` with provenance recorded. Now the bootstrap, the one process holding the harness's real data dir that has just established manifest trust for the cache (an authenticated cache hit, or a fresh download verified against the same-origin manifest), writes `~/.abcd/cache-attestation` — `data_dir`, the manifest-authenticated `binary_sha256`, `cache_trust=manifest`, `attested_at`; 0600, temp-and-rename, beside `path-entry` — and `ahoy install` promotes a cache only when that record names the directory, the co-located record carries the attested hash, and the artefact hashes to it, whichever route (environment or the root's `.data-dir` stamp) named the directory; detection offers the heal on the same predicate. An offline run neither writes nor rewrites the attestation, so a cache provisioned offline waits for a networked session before it reaches PATH, said out loud. Why B: the trust floor moves from a value the environment supplies to a write into the caller's own home, which adr-46 decision 4 already treats as the ownership root, so the attestation grants nothing that authority did not hold and costs `ahoy install` no network (adr-38 stands). Rejected: A (a manifest GET on a disk-only verb, re-fetching what the session already proved); C (a documented residual — weaker than it reads, since a harness honouring a committed settings file's environment block lets a hostile checkout set the variable, and the owned-copy claim is what the hook shims trust); the mechanical partial of cross-checking against the plugin-root binary (breaks dogfood installs whose root binary is a local build); and a terminal rung through the attestation alone (it would heal a dogfood checkout's stable symlink into a release copy). adr-46 is superseded by adr-2609151706587280 (never amended, always superseded; retained because its numbered decisions are cited), spc-35 Design 2 step 1 and Design 3 are revised in place and dated, and brief invariant 12 gains the clause.
- dcn-26092700000000032026-09-27 — iss-2609100506263330 takes the record's option (b): with no verified release artefact in the persistent plugin data directory, `ahoy install` writes no PATH entry and names the install one-liner as the command to run first, rather than writing a symlink into the plugin root that the next plugin update strands (lane implementer drainH, autonomous run A, orchestrator abcd-51). Option (a), fetching the artefact inside install, is not taken: install's documented meaning is local configuration, and adr-38 lets the network answer only a verb whose documented meaning is the fetch; the one fetch-and-verify primitive (`abcd update`'s) also imports ahoy, so reaching it from install would need a second downloader or an import inversion. A symlink into the plugin root that an earlier release wrote is left where it stands on a cold cache, raised as a required `symlink.legacy` gap whatever the cache holds, and recorded so the hooks accept it meanwhile. A dangling link `~/.abcd/path-entry` names (read through the same owned, not-group-or-other-writable guard the hook shims apply) is classified abcd's own and repaired by install or removed by uninstall with its record; an unrecorded dangling link keeps iss-2609100506256636's ruling — no provenance claimed, left untouched by detection and by any run with nothing to write in its place, cleared only when install writes the verified copy there.
- iss-193A managed repo commits under whatever identity git happens to resolve, and when
- iss-294The 'abcd version --check' flag help calls it 'the only command that touches the network', but the same generated reference page also calls 'docs cite refresh' the only network verb and 'memory ingest <url>' does its own HTTP GET — three real fetch paths, contradicting adr-38's own two-verb model
- iss-385commands/version.md's network enumeration is still false after two rewrites — it names version --check and update as the only fetch verbs while docs cite refresh and memory ingest <url> also reach the network
- iss-2608220150157508Local-only token-usage accounting in the history store: at history capture time, extract per-message usage (input, output, cache-read, cache-write) and the model id from the transcript's own usage fields, store the aggregate with a timestamp in the stored transcript's metadata, and offer a report summing per session and per repo. Pricing is a user-level table (~/.abcd), recorded on explicit ask only: when a report meets a model with no recorded pricing it offers an explicit refresh verb in the docs-cite-refresh mould — never an implicit fetch (adr-38). Estimates are labelled API-equivalent (subscription seats do not bill per token; cache tiers price differently). Local-only by construction: never committed, never rendered on any site
- 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-2609251324599468The stubs itd-2609212130136102 leaves for one release are to be removed in the release after it: ahoy dry-run, ahoy identity-check, version, docs lint and site check (deprecated commands), and the bare answers of identity and ahoy remote (whose sub-verbs stay). The spec named a remainder spec minted at close for this, but a remainder close keeps the intent planned while the run closed it shipped, so the removal is held here instead; removing them narrows the surface again and needs the release to declare it.
- iss-2609292057441479itd-111 acceptance criteria 3 and 5 name command spellings the CLI no longer answers to: AC3 says the vintage report comes from 'abcd version' and AC5 says the explicit release check is 'abcd version --check' (design decision 6 and its open question say the same). Since itd-2609212130136102 consolidated the verbs, the report is 'abcd --version' and the check is 'abcd update --check'; both old spellings refuse with a moved notice naming the new one. The behaviour each criterion asks for is present at the new spelling, so the shipped criteria read true only once the record says which surface meets them. adr-38 tier 2 also cites 'abcd version --check' as an example of an explicit ask; the rule it states still holds.
- iss-2610141706058429commands/update.md network inventory (adr-38) says GitHub attestations API and the Sigstore TUF repository are reached by update, to verify build provenance, reading as exhaustive; ahoy install now makes the same check (internal/core/ahoy/apply.go), and both verbs also reach the GitHub git API to resolve the tag commit (internal/core/update/provenance.go), which the inventory omits.
- itd-66abcd Renders The Exact Public Payload, Proves The Excludes Never Leak, And Smoke-Tests The Installed Surface Before Any Snapshot
- itd-84An Intent Is Decomposed Before It Is Filed, And A Reversal Is Only Ever Flagged
- itd-130`abcd update` completes a chosen update in one verb
- itd-131The managed-repo identity gate: the human is the author of record on every commit, before the first one
- itd-2609151838312703Sessions under any account, on any machine or in the cloud announce what they are doing through short fixed-form announcements on the shared remote, a person reads everyone's status with one command, and what is really a hold stays a reservation
- itd-2609212130136102abcd's verbs consolidate: modes become flags and five checks become one lint
- itd-2609292107351737A development session's token cost is measured, and set against the size class its spec predicted
- itd-2609292108089653Ingesting and consulting sources are abcd verbs, and no corpus entry is a keyword stub
- itd-2610031026190632abcd's update check says when a harness is older than abcd needs
- itd-2610031651058674abcd's check reports README parts that have gone stale, and writes nothing
- itd-2610032150577708The product thinker's dashboard, in a browser through Tailscale
- itd-2610091010348892An opted-in project's record is built at each release onto its own GitHub Pages
- spc-22a-stale-abcd-never-answers-silently-every-surface-that-runs
- spc-32`abcd update` — fetch, verify, swap, in one verb
- spc-34The managed-repo identity gate — detect the wrong author/committer, establish only on confirm
- spc-2610031342374947when-a-person-runs-abcd-s-check-on-purpose-abcd-also-says
- spc-2610100613109045a-person-who-types-abcd-sees-only-the-commands-people-use