Development record
Now that transcripts are captured automatically on every session end, the scanner's secret-pattern coverage becomes load-bearing in a way it was not when capture was a manual verb nobody ran. Verified by live test: the bundled patterns DO catch anchored tokens (AKIA... access key IDs, ghp_/gho_/sk-ant- style prefixes) and absolute home paths, but they do NOT catch unanchored high-entropy values — an AWS SECRET access key (the 40-char base64 value, no prefix), a bare password, or a generic API token with no recognisable prefix all pass through into the store verbatim. This is the standard prefix-matching limitation and is pre-existing, not a regression; the point is that the blast radius changed. Consider entropy-based detection or the opt-in gitleaks adapter for the transcript path specifically, where the input is unstructured prose rather than curated source.
Now that transcripts are captured automatically on every session end, the scanner's secret-pattern coverage becomes load-bearing in a way it was not when capture was a manual verb nobody ran. Verified by live test: the bundled patterns DO catch anchored tokens (AKIA... access key IDs, ghp_/gho_/sk-ant- style prefixes) and absolute home paths, but they do NOT catch unanchored high-entropy values — an AWS SECRET access key (the 40-char base64 value, no prefix), a bare password, or a generic API token with no recognisable prefix all pass through into the store verbatim. This is the standard prefix-matching limitation and is pre-existing, not a regression; the point is that the blast radius changed. Consider entropy-based detection or the opt-in gitleaks adapter for the transcript path specifically, where the input is unstructured prose rather than curated source.
Verification (2026-08-01, item A6 of the v0.5.0 plan). Re-checked against the
network-identifier pattern set landed by A2, which is folded into
DefaultPatterns and therefore inherited by the transcript path. Outcome: one
class newly covered, the three classes this entry names confirmed as residue by
test rather than by inspection. The entry stays open, re-scoped below.
Newly covered since capture. Nothing in the entropy classes, but the
transcript path gained a detection class it did not have when this was filed:
non-reserved IPv4/IPv6/MAC addresses (net:ipv4, net:ipv6, net:mac,
hard_fail) and LAN/device hostnames (net:lan_hostname, net:device_hostname,
warn), by allowlist inversion over the reserved documentation ranges. All five
kinds are exercised on the transcript path itself in the corpus below, not
inferred from their presence in DefaultPatterns. A leaked machine identifier is
therefore newly covered on this path since this entry was captured. It was never
part of the residue — line 12 above names unanchored secrets and home paths only,
never a network identifier — so this is coverage the re-check establishes, not a
gap it closes.
Residue, verified empirically. Synthetic specimens presented to the
transcript path's own entry point — (*Scanner).ScanText, the call
internal/core/history.Capture makes at stage one — produce zero findings:
- bare secret-access-key value (40 characters, no prefix), both with and without
an
AWS_SECRET_ACCESS_KEY=key name in front of it — passes through; - bare password, both after
password:and after--password=— passes through; - prefix-less API token (32 hex characters after
api_key =; 40 base64 characters afterAuthorization: Bearer) — passes through; - a genuinely high-entropy 40-character alphanumeric value (5.32 bits per
character, assembled at run time from a fixed-seed shuffle), both bare and
after an
api_key =key name — passes through.
Anchored controls in the same corpus are caught (token:aws_access_key,
token:github_pat, token:anthropic, token:pem_private_key,
home_path_self, home_path_other, plus all five network kinds above), so the
specimens reach a working detector; one of those controls sits inside the
negative test too, so an emptied pattern set cannot satisfy it vacuously. The
TOKEN patterns are prefix-anchored apart from rp_session_key, which keys on one
literal JSON field name ("sessionKey") rather than on a generic key-name class,
and no pattern measures entropy — so a password:, api_key = or
Authorization: Bearer key name beside a value does not help. The set as a whole
is not uniformly prefix-anchored, and the entry should not be read as saying so:
the network kinds are an allowlist inversion over the reserved documentation
ranges, the identity kinds key on the probed identity, token:pem_private_key is
a literal header, and Pattern.SkipAt (in
internal/adapter/scanner/patterns.go) already lets a pattern accept or
reject a match by what SURROUNDS it on the line. None of those mechanisms reaches
an unlabelled value, which is why the residue stands.
Pinned in the tree, so the residue is evidence rather than a claim:
TestTranscriptPathMissesUnanchoredEntropy
(internal/adapter/scanner/transcript_coverage_test.go) at the pattern-set
boundary, and TestCaptureStoresUnanchoredEntropyVerbatim
(internal/core/history/history_test.go) at the store boundary, which captures a
six-line transcript carrying an anchored token on the first line and the
unanchored specimens on the lines below it, then asserts each specimen present
verbatim in the written record while the anchored token is absent AND its
fingerprintSpan fingerprint present — that is the byte-level mirror of
maskSecret, and it is what the Capture path actually produces, via sealLine;
absence alone would also be satisfied by a
store that dropped the line. The same capture asserts the contra case, so the
class A2 did close is evidence rather than prose: a flagged private address on
the last line is absent from the stored record and its [redacted-address]
placeholder present.
What the pins can and cannot catch. They assert CURRENT behaviour, and their
reach is the PATTERN SET this path runs. They alarm reliably for growth in that
set — a charset/length floor, key-name context matching, any new DefaultPatterns
entry THAT REACHES THESE SHAPES, and now a genuine entropy floor. (The
qualification matters and the test file states it: an entry that reaches nothing
in the corpus leaves both pins green, and rightly so.) That last one needed fixing: the original
specimens all repeat, and measure 3.12 (FAKEfake00×4), 2.16 (deadbeef×4) and
2.75 (base64 run) bits per character, all BELOW the ~3.5-bit threshold an entropy
detector conventionally uses, so such a detector could have landed with every pin
still green. A high-entropy specimen was therefore added at both boundaries —
5.32 bits per character, assembled at run time from a fixed-seed shuffle of the
alphanumeric alphabet, with its measured entropy asserted in-test (at or above
4.5 bits per character) so it cannot quietly degrade into a repetitive value, and
its exact value asserted against a golden constant on BOTH sides, so the two
duplicated generators cannot silently diverge and determinism is pinned as well.
They CANNOT alarm for option (c) below: an opt-in external-scanner adapter never
consults DefaultPatterns, so ScanText keeps returning nothing and both pins
stay green while coverage has in fact grown. That route must be re-pointed by
hand, and the pins alone are therefore not grounds to close this entry.
Re-scoped to a decision, not an implementation. What remains is a choice between three options that differ in REACH, not only in false-positive cost. Stating them as equal-coverage-at-different-cost would misdescribe them:
- (a) an entropy/charset-class detector with a length floor. The only one of the three that reads the VALUE, so the only one that reaches an unlabelled value: it would cover every specimen above, including the bare 40-character secret-key shape with no key name beside it.
- (b) key-name context matching (
password,secret,token,api_keyto the left of a delimiter). Labelled values ONLY. It is structurally incapable of this entry's own first-named case — a bare secret-access-key value with no key name, pinned as the case "bare 40-char secret-key shape, no key name" — because there is no key for it to match. Its merit is elsewhere: it generalises mechanisms the set already ships rather than introducing one, sincerp_session_keymatches on a literal field name today andPattern.SkipAtalready decides a match by its surrounding line. Note against the measurement below: (c) as shipped already reaches most of what (b) would, so (b)'s distinct increment over (c) is narrow — labelled values that (c)'s delimiter or entropy conditions exclude — and its real argument is that it is native, in-tree and carries no external dependency, not that it reaches further. - (c) the opt-in external-scanner adapter over the transcript path only. Reach is whatever the adapter's ruleset delivers — a measurement, not an assumption. Measured below: MORE than (b), not less.
The (c) measurement, re-run 2026-08-01. An earlier measurement recorded on
this entry was wrong in both its fixture and its conclusion, and is superseded by
this one. Method, so it can be repeated: gitleaks 8.24.3, the release binary
gitleaks_8.24.3_linux_x64.tar.gz verified against the release checksums.txt
(sha256 9991e0b2903da4c8f6122b5c3186448b927a5da4deef1fe45271c3793f4ee29c) — the
same version this repo's own history scan pins in .github/workflows/ci.yml —
run as gitleaks dir with its DEFAULT rules, no repo config. The fixture is one
specimen per line, and it is named precisely: every case of the table in
TestTranscriptPathMissesUnanchoredEntropy
(internal/adapter/scanner/transcript_coverage_test.go), every line of the
transcript TestCaptureStoresUnanchoredEntropyVerbatim captures
(internal/core/history/history_test.go), and delimiter probes on the same
values, added to isolate the mechanism. Twenty-one lines, six findings, all
generic-api-key. Enumerated in full below — sixteen rows for twenty-one lines,
because a shape carried by both fixtures is merged into one row (differing only
by the store transcript's conversational framing: its user: /assistant:
prefixes, and on row 1 its prose lead-in around the same anchored token), and
rows 10 and 13 merge the two undelimited key names. (<hi> is the 40-character
high-entropy specimen, <pass> the five-word passphrase; neither value appears
here. The entropy column is gitleaks' reported measurement on findings and the
same Shannon computation on passes — identical arithmetic, since gitleaks
reports no figure for a line it does not flag.)
| # | line as scanned | from | gitleaks 8.24.3 |
|---|---|---|---|
| 1 | token=ghp_ + F×36 |
both fixtures | passed (see caveat) |
| 2 | AWS_SECRET_ACCESS_KEY= + FAKEfake00×4 |
both fixtures | passed — entropy 3.12 |
| 3 | FAKEfake00×4, bare |
scanner corpus | passed — no key name |
| 4 | password: + <pass> |
both fixtures | CAUGHT generic-api-key, entropy 3.66 |
| 5 | --password= + <pass> |
scanner corpus | CAUGHT generic-api-key, entropy 3.66 |
| 6 | api_key = + deadbeef×4 |
both fixtures | passed — entropy 2.16 |
| 7 | Authorization: Bearer + Zm9vYmFy×5 |
scanner corpus | passed — entropy 2.75 |
| 8 | <hi>, bare |
scanner corpus | passed — no key name |
| 9 | api_key = + <hi> |
scanner corpus | CAUGHT generic-api-key, entropy 5.32 |
| 10 | session_token + <hi> (no delimiter) |
store fixture | passed — no =/: |
| 11 | session_token = + <hi> |
delimiter probe | CAUGHT generic-api-key, entropy 5.32 |
| 12 | session_token: + <hi> |
delimiter probe | CAUGHT generic-api-key, entropy 5.32 |
| 13 | session_token / api_key + <hi> (no delimiter) |
delimiter probe | passed — no =/: |
| 14 | session_token = + FAKEfake00×4 |
delimiter probe | passed — entropy 3.12 |
| 15 | api_key = + Zm9vYmFy×5 |
delimiter probe | passed — entropy 2.75 |
| 16 | ssh + private IPv4 quad |
store fixture | passed — not a credential rule's business |
The rule doing all the work is generic-api-key, and its condition is a
conjunction of three things — a key-name KEYWORD (key, token, secret,
password, auth, api, …), a DELIMITER (=, :, and relatives) between
keyword and value, and Shannon entropy of at least 3.5 bits per character in
the value — subject to the ruleset's own stopword allowlist, which suppresses a
value containing a dictionary marker such as fake or deadbeef regardless of
its entropy. Rows 4, 5, 9, 11 and 12 satisfy all three. Every other row fails at
least one: rows 3 and 8 have no keyword; rows 10 and 13 have a keyword but no
delimiter; rows 7 and 15 have keyword and delimiter but sit under the entropy
floor; rows 2, 6 and 14 sit under the floor AND carry the fake/deadbeef
stopwords, either of which suppresses them alone — raising their entropy without
changing their spelling would not flip them.
(c)'s reach, stated accurately. Keyword + delimiter + entropy ≥ 3.5 reaches
LABELLED, DELIMITED, high-entropy values — including the branch's own
api_key = <hi> corpus case. It therefore SUBSUMES much of option (b) and adds
an entropy floor on top of it, rather than being the narrowest of the three. It
does NOT reach a bare value with no key name (rows 3, 8 — this entry's own
first-named case), a key name separated from its value by prose rather than a
delimiter (rows 10, 13 — exactly the store fixture's hand-built
session_token <hi> line), or any labelled value below the entropy floor (rows
2, 6, 7, 14, 15 — every repetitive specimen in the corpus, whatever key name
precedes it).
Caveat on row 1, verified rather than assumed: the anchored ghp_ control
passed because its payload is one character repeated 36 times, which gitleaks'
own entropy filter drops. A realistic-entropy ghp_ token in the same run IS
caught, by the github-pat rule. Row 1 is a fact about this fixture, not about
that ruleset's coverage of real prefixed tokens.
What the earlier measurement got wrong, recorded so the correction is not
mistaken for a change in the tooling. It named "the store fixture's own specimen
set" and then listed a base64 token among the passes — but the store fixture
carries no base64 token (Zm9vYmFy×5 lives only in the scanner corpus), so the
fixture it described did not exist. And it read the high-entropy miss as a
failure of key-name context ("despite a session_token key name beside it"),
which inverted the causal claim: the miss is the absent DELIMITER on that
hand-built prose line, as rows 10 to 13 isolate. The consequence is a materially
different answer — (c) reaches labelled high-entropy values, so it is not the
narrow option the earlier passage implied.
False-positive cost is the second axis, and it lands hardest on (a): a transcript is full of hashes, base64 blobs and identifiers that are not credentials, and a redaction false positive corrupts the record redaction exists to preserve. (c) carries a share of that cost too, in proportion to its reach — its entropy floor fires on any labelled high-entropy token in prose, credential or not. Reach and cost together are the open question, and where the bar sits is the maintainer's to decide — grill-then-implement, not autonomous work.
Re-check (2026-09-26, autonomous run A, lane drainS1). Nothing built for
this entry: the 2026-08-28 ruling already rejected option (a) and shipped (c)
opt-in, and the residue it names is exactly what only (a) reaches, so building
a detector would be re-opening a ruling rather than implementing one. The
lane's JSON-escape views (iss-2609261647358395) widen the ANCHORED reach on
this path — a prefixed token written straight after a \n or \t escape in a
raw transcript line is masked from that change on — and leave the
unanchored residue exactly where it was: TestTranscriptPathMissesUnanchoredEntropy
and TestCaptureStoresUnanchoredEntropyVerbatim both still pass.
Remedy grounds (2026-09-29)
- Why: ruling AK is unanswered, so both answers are written; the native rule is the transcript-only detector the owed options name, sized to the reach the gitleaks measurement on this record shows, and it keeps the 2026-08-28 rejection of a value-only entropy detector intact.
- Sources (consulted 2026-09-29): detect-secrets documents default entropy limits of 4.5 for base64 and 3.0 for hex strings and a KeywordDetector for 'non-secret-looking' values (https://github.com/Yelp/detect-secrets); gitleaks' generic-api-key rule combines keyword, delimiter and entropy, and its tracker records false positives on commit hashes and key-like identifiers (https://github.com/gitleaks/gitleaks/issues/1578, https://github.com/gitleaks/gitleaks/issues/1830), the transcript prose cost the ruling weighs.
- Rejected: an always-on value-only entropy detector, rejected on 2026-08-28 for corrupting records with redacted hashes and ids.
Grounds
- declined: Ruled by the product thinker on 2026-10-11 (ruling AK): keep today's shape-based redaction of stored transcripts and add no keyword-delimiter-entropy rule, so a secret with no known shape is stored as written; the two existing pins stay the alarm for the gap. Under abcd-fixes-what-it-adds this is abcd's own protection, and the ruling chose no ordinary text hidden over catching unknown-shaped secrets.
Frontmatter
| id | iss-96 |
|---|---|
| type | issue |
| lifecycle | wontfix |
| severity | minor |
.abcd/work/issues/wontfix/iss-96-now-that-transcripts-are-captured.md
open on GitHub ↗ · commit history ↗
Referenced by1
- iss-125Hostnames are absent from the Stage-1 redaction pattern list: itd-28's write-time sanitiser enumerates home-dir paths and provider secrets (AWS/GCP/Azure/Cloudflare/GitHub/Anthropic/OpenAI/Stripe/Slack/JWT/PEM) but no hostname patterns, and the transcript scanner's coverage gaps are already tracked as iss-96 — yet the PII discipline forbids posting real hostnames anywhere. Live specimen: a saved third-party PR page (reviewed 2026-07-25) shows two real machine hostnames leaked into public PR comments by a resident review fleet, including a '.fritz.box' home-router LAN name — exactly the class a Stage-1 net must catch before any abcd-produced artefact (review receipt, rendered PR comment, transcript) leaves the machine. Fix belongs in the scanner seam's canonical pattern set (one-canonical-primitive), not a per-surface copy; detector first: add hostname fixtures (LAN suffixes like .local/.fritz.box/.lan, machine-name shapes) to the scanner's acceptance corpus and watch them flag before widening the patterns.blocks
mentions11
- dcn-26071400000000082026-07-14 (M1) — iss-95: wiring the hook does NOT by itself start the clock. `history.
- dcn-26072400000000042026-07-24 — itd-28 gitleaks sign-off: Stage 2 runs through the scanner
- dcn-26080100000000012026-08-01 — iss-96 (item A6), a VERIFICATION milestone rather than an implementation: the transcript scanner's tracked coverage gaps re-checked against the network-identifier set A2 folded into `DefaultPatterns`, and the residue re-scoped in place. Two findings, both empirical rather than inspected. (1) A2 DID reach this path — every scanner consumer inherits the network set, so a non-reserved address or a LAN/device hostname in a captured transcript is now a finding (all five kinds — `net:ipv4`, `net:ipv6`, `net:mac`, `net:lan_hostname`, `net:device_hostname` — exercised on the transcript path itself in the same corpus, not inferred from their presence in the pattern set, and a flagged private address asserted REDACTED in the stored record at the store boundary); that class is therefore newly covered on this path since iss-96 was captured — it was never part of the entry's residue, which names unanchored secrets and home paths only, so this is coverage the re-check ESTABLISHES rather than a gap it closes. (2) The classes iss-96 actually names — a 40-character secret-key value with no prefix, a bare password, a prefix-less API token, and now a genuinely high-entropy 40-character value — still produce ZERO findings at the transcript path's own entry point, with or without a `password:`, `api_key =` or `Authorization: Bearer` key name beside them, because the TOKEN patterns are prefix-anchored APART FROM `rp_session_key`, which keys on the literal JSON field name `"sessionKey"` rather than on a generic key-name class, and no pattern measures entropy. The set as a WHOLE is not uniformly prefix-anchored and must not be described as such: the network kinds are an allowlist inversion, the identity kinds key on the probed identity, `token:pem_private_key` is a literal header, and `Pattern.SkipAt` already lets a pattern accept or reject a match by what SURROUNDS it — none of which reaches an unlabelled value, which is why the residue stands, and two of which (`rp_session_key`, `SkipAt`) are the shipped precedent remediation option (b) would generalise rather than introduce. Disposition: iss-96 stays OPEN with a dated verification section appended in place (the iss-24 precedent; a `capture resolve` would have had to state an action nobody took, and the ledger's resolution field is exactly that claim), re-scoped from "coverage may have moved" to a single unresolved DECISION between an entropy/charset detector with a length floor, key-name context matching, and the opt-in external-scanner adapter over this path only — three options that differ in REACH, not merely in false-positive cost, and saying otherwise would misdescribe them: (a) reads the value, so it is the only one that reaches an UNLABELLED value; (b) covers labelled values only and is structurally incapable of this entry's own first-named case, a bare secret-key value with no key name, because there is no key to match; (c) has whatever reach its ruleset delivers, and the measurement recorded in the entry corrects an earlier one that was wrong in both fixture and conclusion — re-run 2026-08-01 with pinned, sha256-verified gitleaks 8.24.3 default rules (`gitleaks dir`, no repo config) over a NAMED fixture, one specimen per line, comprising every case of `TestTranscriptPathMissesUnanchoredEntropy`'s table, every line of the transcript `TestCaptureStoresUnanchoredEntropyVerbatim` captures, and delimiter probes on the same values: 21 lines, 6 findings, all `generic-api-key`, whose condition is keyword + `=`/`:` DELIMITER + entropy at or above 3.5 bits per character, so `password: <passphrase>`, `--password=<passphrase>`, `api_key = <high-entropy>` (the branch's own corpus case), `session_token = <high-entropy>` and `session_token: <high-entropy>` are all CAUGHT, while bare values with no key name, key names separated from their value by prose rather than a delimiter (the store fixture's hand-built `session_token <high-entropy>` line, which the earlier measurement mis-read as a key-name failure when the cause is the absent delimiter), and every sub-floor repetitive specimen whatever key name precedes it, all pass — so (c) as shipped reaches LABELLED high-entropy values, subsuming much of (b) and adding an entropy floor on top of it rather than being the narrowest option, and (a) remains the only one that reaches an UNLABELLED value; the anchored `ghp_` control passing is a fixture artefact (36 repeated characters fall under gitleaks' own entropy filter — a realistic-entropy `ghp_` token in the same run IS caught by `github-pat`), not a coverage claim. False-positive cost is the SECOND axis and lands hardest on (a) — a redaction false positive corrupts the record redaction exists to preserve — so reach and cost together are the open question and the bar is the maintainer's to set: grill-then-implement, not autonomous. The verification is PINNED in the tree rather than asserted in prose: `TestTranscriptPathMissesUnanchoredEntropy` at the pattern-set boundary and `TestCaptureStoresUnanchoredEntropyVerbatim` at the store boundary (each specimen present verbatim in the written record, the anchored token absent AND its `fingerprintSpan` fingerprint present — the byte-level mirror of `maskSecret` that `sealLine` actually applies on the Capture path, not `maskSecret` itself — since absence alone would also be satisfied by a store that dropped the line). THE PINS' REACH IS THE PATTERN SET, and the earlier claim that they "fail by design, never a silent pass" was too broad on two counts, both now fixed: the original specimens all REPEAT and measure 3.12, 2.16 and 2.75 bits per character, below the ~3.5-bit floor an entropy detector conventionally uses, so an entropy detector could have landed with every pin green — a 5.32-bit specimen assembled at run time from a fixed-seed shuffle, with its entropy asserted in-test at or above 4.5 bits per character AND its exact value asserted against a golden constant on both sides (entropy alone cannot see the two duplicated generators diverging, since a changed stream would still measure ~5.32 bits; the golden also pins determinism, and is gitleaks-clean as a literal because a bare 40-character alnum run carries no keyword and no delimiter), now closes that hole; and option (c) never consults `DefaultPatterns` at all, so NO ScanText-level pin can alarm for it by construction, which is stated in the entry so a stale close cannot lean on the pins alone. Within the pattern set they do fail by design — a charset/length floor, key-name matching or any new `DefaultPatterns` entry THAT REACHES THESE SHAPES trips them (the qualification is the test file's own, and it matters: an entry reaching nothing in the corpus rightly leaves both pins green) — and that failure is the signal to re-point them and close the entry. Never close on assumption: the anchored controls in the same corpus are caught, and one of them sits INSIDE the negative test, which is what proves the specimens reached a working detector rather than a mis-wired or emptied one; the negative assertion is scoped to the token/secret kind family, so an unrelated widened pattern fails it under a DIFFERENT message rather than being credited as entropy coverage — and that message now names BOTH readings, because a secret or entropy detector shipping under a namespace the family list does not know (`secret:entropy`, say) arrives down the same branch, where telling the reader to adjust the specimen would hide the very coverage growth the pin exists to notice. Every credential-shaped specimen is assembled at run time — the discipline network_test.go already states for flagged network identifiers — so, beyond the one golden constant this entry discloses as measured clean, no literal of that shape enters this repo's history for a full-history secret scan to fire on.
- dcn-26080100000000042026-08-01 — guard batch (iss-159/144/148), REVERT of the backtick sub-part after a THIRD adversarial round; the other three sub-parts stand. Following backtick command substitution went in, then grew a frame/stack mechanism intended to hold two properties at once — a substitution's content checked as its OWN segment (so a hazard hidden inside one is matched) while the ENCLOSING command's tokens, chain index and command position survived the boundary. Three rounds, three fresh independent reviewers, three BLOCKER findings, all in that mechanism rather than in one instance of it: (1) a substitution before trailing flags truncated the enclosing segment, so `rm` + substitution + `-rf *` went block to allow; (2) the frame fix for (1) renumbered the enclosing command's chain when a newline fell inside a substitution, defeating every `after_cd` entry (`cd scratch && rm -rf $(\nfind . -name x\n)`); and (3), the one that decided it, a substitution in LEADING command position left a bare `$` as the enclosing command's argv[0], so `commandOf` returned `"$"` and NO entry could match — that is not a hole in `rm-rf-after-cd-chain`, it is a hole in the registry, reachable by prefixing any hazard with `$(true)`. Two more in the same round: a bare `(` nested inside `$( … )` mis-popped the frame and re-opened (1) in a narrower shape, and an unterminated backtick failed OPEN. DECISION: revert rather than patch a fourth time inside one round. A mechanism that has produced a security-relevant regression on every attempt is not converging, and each fix has been strictly worse than the last in blast radius — the third defeats the entire registry. `internal/core/guard/tokenize.go` is restored byte-for-byte to `main`: no backtick handling at all (literal text), `$( … )` exactly as it was, and the four guard.json fixtures the frame mechanism added removed with it. The two claims of the entry above are therefore WITHDRAWN, not superseded: backtick substitution does NOT reach command position, and the enclosing command does NOT survive a substitution. The literal ledger ask ("a backtick command substitution is not followed, while the dollar-paren form is") asked for PARITY with `$( … )` as it behaves today, and parity is what leaving both forms untouched delivers — the gap is disclosed again as a stated v1 limit on all four coverage surfaces (brief, plugin command, CLI long help + generated reference, package map), which is the honest form of it. Recorded rather than quietly carried: `$( … )` on `main` already loses the flags written AFTER a substitution — `rm $(true) -rf *` does not read as a recursive force delete — a pre-existing gap this round's review process surfaced and did not introduce, undisclosed before it and undisclosed still, because closing it is the same design problem as following a backtick and the two should land together or not at all. iss-148 is re-scoped IN PLACE back to open on that one sub-part (the iss-96 precedent: a `capture resolve` would have to state a resolution for scope that is not settled), with the three shipped sub-parts and their commits named in the entry; iss-144 and iss-159 are untouched and stay resolved, neither being implicated in any review finding. Deferred as its own future work, and explicitly NOT a patch to the reverted code: a substitution model that follows the payload AND preserves the enclosing command across the boundary, which needs the tokenizer to carry a command-position stack rather than a segment flush — the reason three attempts to bolt it onto the flush all leaked.
- dcn-26082800000000022026-08-28 — Transcript deep-secret coverage lands as an OPT-IN gitleaks adapter, off by default (iss-96). The native scanner stays the always-on default; a repo arms deeper coverage by dropping an enabled `.abcd/config/gitleaks.json`, whereupon `internal/adapter/gitleaks` shells out to a gitleaks binary (PATH or configured path) over the transcript and its findings AUGMENT native redaction through the same `scanner.Redact` discipline. This realises option (c) of the iss-96 grill (labelled+delimited+high-entropy reach the prefix-anchored native set misses) as the host-delegated/opt-in-adapter pattern (adr: LLM/CLI oracles are opt-in adapters), NOT an always-on entropy detector (option (a), rejected for its redaction false-positive cost on prose) and NOT a hard gitleaks dependency in the default build. Fail-closed, never a silent no-op: an armed-but-absent binary makes capture refuse the write with `gitleaks configured but not found`. Default-off invokes nothing — no lookup, no process, no cost — so the unopted path is byte-for-byte unchanged. iss-96's residue beyond (c)'s reach (bare no-keyname values, sub-entropy-floor tokens) stays open.
- iss-148guard registry coverage gaps found while wiring itd-103 (registry content, not matching semantics): a push whose refspec carries a leading plus is a force in disguise and no entry describes it; xargs, timeout and exec are absent from the matcher's wrappers set, so a hazard launched through one of them is not seen; a backtick command substitution is not followed, while the dollar-paren form is; and a wrapper that IS in the set defangs an entry the moment it carries its own flags, because only the wrapper name is stepped over — `sudo <hazard>` is seen, `sudo -u bob <hazard>` is not, and the same holds for `env -i` and `time -p`. That last one is the sharpest: it turns an entry the registry does describe into an allow with one extra token, and it is the only item here that a facilitator would reasonably assume was covered. Candidates for the admission gate as the registry grows from reality; the wrapper-flag item is matcher-side (`commandOf` in internal/core/guard/match.go), not registry content.
- iss-217add a cross-agent session-capture source as an optional plugin
- iss-2609020127210042The PEM block consumer redacts the common renderings of a key body but not every one, and one armour form is not detected at all. scanner.pemBodyLineRe accepts an optional gutter then a single base64 run, so a body rendered as log-prefixed lines (a timestamp/level prefix), as CSV cells, as one element per XML tag, with a trailing hash comment, as a source-string concatenation, as a JSON array on one line, or with double-escaped backslash-n separators is not body-shaped, the block ends at that line and the rest is stored verbatim; the tail of a block longer than maxPEMBodyLines survives by the same route. The RFC4716 armour (four dashes, BEGIN SSH2 ENCRYPTED PRIVATE KEY) does not match the bundled pem_private_key pattern at all, so no block is ever opened for it. Evidence: scanner.pemBodyLineRe and maxPEMBodyLines in internal/adapter/scanner/pem.go, DefaultPatterns pem_private_key in patterns.go. A fix must either widen the body-shape rule over these renderings or arm an entropy rule for a headerless base64 run (iss-96), and add the RFC4716 armour to the header pattern.
- iss-2609261647358395A JSON string escape beside a secret or a home path hides it from the scanner's raw-line pass, and the transcript store scans raw JSONL. Every bundled token pattern anchors on a leading word boundary, so a token written straight after the \n, \t, \r, \b or \f escape (a GitHub PAT at the start of a line of pasted output, JSON-encoded) has the escape letter as a word byte before it and never matches: ScanText at BASE 211b8853 returns nothing for {"t":"tok\nghp_..."} while the same token after a space is token:github_pat. The identity matchers fail the same way: home_path_other finds no third-party home path that follows a \n or \t escape (the leading anchor reads the escape letter as a path byte) or that precedes a \n or \" escape (the trailing boundary set has no backslash), and a non-ASCII real_name written by an ASCII-only encoder (Python json's default \u00e9) never matches the configured name. The fix reads a line's JSON-decoded layers the way the percent-decode pre-pass reads its percent-decoded copy, mapping each hit back to its raw span. Detector: a token after a \n escape, a home path between \n and \" escapes and a \u-escaped real_name are each a finding on the transcript path.
- iss-2610280103498613The native secret patterns miss several prefix-anchored vendor tokens, which capture writes into the committed ledger
- itd-28Spec-Tied Reviews Live Next To The Spec They Reviewed