fix(docs): establish a verbatim fetch route so a truncated env-vars read stops reading as drift - #2185
Conversation
… reading as drift `env-vars` cannot be read whole through a summarizing fetch. Three independent attempts truncated before the `CLAUDE_CODE_MAX_*` range, and the summarizer then reported those rows absent — a false negative indistinguishable from real drift. This repo cites `env-vars` in ~13 places, so the defect was standing, silent, and reproducible on demand. The route: `curl` the raw-markdown channel (`<page>.md`) to a file and search the file. Verified on `env-vars` — 361,797 bytes, 458 lines, 318 variable rows including the range that had truncated away three times, two fetches hashing identically. It is hoisted, not invented: `claude-ops`'s `changelog` skill already carried it page-scoped and `knowledge`'s publisher profile already preferred the `.md` channel, so per one-owner-per-concern the general form lands in the upstream-drift convention and their page-specific detail stays with them. The mirror rung from #2182 is kept as the fallback with its freshness-corroboration step and its one-rung-below label. Then every `env-vars` claim in the repo was re-derived from that read. Two were wrong. `audit`'s MANDATORY env-var check instructed auditors to do precisely the thing that fabricates findings, with no truncation guard and a framing that also invited the inverse error — three variables this repo cites as real are absent from the page. `audit-pass` carried `DISABLE_DOCTOR_COMMAND` as undocumented; it is documented, and describes that skill exactly. `docpage-digest` cited the page by line number, which had moved. `sweep-all`'s mirror basis retired to a primary one, by the terms of the trigger #2182 wrote a day earlier — the convention working as designed, which is the argument for writing retirement conditions down.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @kyle-sexton's task in 4m 10s —— View job Review complete
SummaryThis is a docs-only PR (17 files, all markdown/JSON) adding a One correctness finding, posted inline:
Note on verification: I don't have network egress in this environment ( No other correctness, architecture, or maintainability issues found in the diff. |
|
Claude finished @kyle-sexton's task in 51s —— View job Security review of PR #2185
Scope reviewed: all 17 changed files ( FindingsNone. This PR is documentation-only:
No security concerns to flag on this PR. |
|
Claude has reviewed this PR 4 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1256cd90bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…links installable Review round 1. Two findings, both real. The 315-vs-318 split was mine: the sed that corrected the count matched only unwrapped occurrences, so two line-wrapped ones in `knowledge` kept the wrong figure. 315 is the variables table; 318 counted three settings-file rows in a different table on the same page. On a PR whose thesis is no-verbatim-quote-no-claim, an unreconciled count is the defect it argues against. The relative `../../docs/` links do not survive plugin installation — the cache holds `plugins/<name>`, not the repo-level docs tree — so every link added inside a plugin is now an absolute GitHub URL. Vendoring the guidance instead would violate pointer-not-copy; the operational rule is stated inline at each site regardless, so an installed reader is never dependent on following the link. Two pre-existing CHANGELOG links the rewrite caught are restored: history is never rewritten. Reviewing that file also surfaced prior art the PR had understated. The Anthropic publisher profile ALREADY bound absence-establishing fetches to curl-the-.md, on the asymmetry that a truncated fetch cannot fabricate a presence, only an absence. The duplicate paragraph is withdrawn, the rule is left where it is and named as the fleet rung, and the convention now says the route was hoisted from two surfaces that derived it independently — which is a stronger claim than one surface plus an invention. Its recorded earlier read of the same page (451 lines, 316 rows, versus 458 and 315 today) is now the evidence for the line-numbers-rot rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
|
Claude finished @kyle-sexton's task in 1m 4s —— View job Security review of PR #2185 (re-run after new push, commit
|
|
Claude finished @kyle-sexton's task in 1m 59s —— View job Code review complete
SummaryThis synchronize pushed one commit,
No new correctness issues found in this round. Both findings from the prior review are resolved; I have no other findings to add. Not independently verified (no network egress in this environment, same limitation as the prior review round): the underlying |
The line-numbers-rot rule was argued from a row-count delta -- 316 rows in the profile's earlier read of env-vars.md against 315 today -- and concluded that both a growth and a removal landed between the reads. That conclusion is not available: the earlier 316 has no recorded counting rule, and this branch just established that two defensible rules on this page differ by three (315 variable rows, 318 including a second table's settings-file rows). If the earlier author counted the way the first draft here did, the delta runs the other direction and needs no removal at all. The line numbers are unambiguous and both are already on the record: that read puts CLAUDE_CODE_MAX_OUTPUT_TOKENS at line 277 of 451, and the 2026-08-10 read puts the same row at line 280 of 458. That proves line numbers rot, which is the only thing the rule needs, and it assumes nothing about anyone's counting rule. Same defect class the review caught one commit ago -- a number reused across surfaces without its derivation -- and it does not belong in the PR that establishes no-verbatim-quote-no-claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
|
Claude finished @kyle-sexton's task in 44s —— View job Security review of PR #2185 (commit
|
|
Claude finished @kyle-sexton's task in 2m 41s —— View job Code review complete
SummaryThis review covers the state as of
New finding (posted inline)The latest commit (
Other checks
|
…f deleting it Review round 2: the retracted row-count inference had a sibling three lines below -- the profile's own record of an earlier read, 'a 451-line, 316-row page'. Same weakness: no counting rule, on a page that admits two differing by three. Qualified rather than removed. That figure is this pipeline's recorded observation of a fetch it performed, and the profile's own archive-reading rule says a source artifact is noted at the row and never silently repaired -- deleting the number would repair the record instead of annotating it. What the line now says is what is true: the count supports nothing by subtraction, and the rule it appears in rests on the 277-of-451 position and the first-fifth cutoff, which are unambiguous. That is also why the sibling rule above argues from line numbers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
|
Claude finished @kyle-sexton's task in 38s —— View job Security review of PR #2185 (commit
|
|
Claude finished @kyle-sexton's task in 4m 3s —— View job Code review complete
SummaryReviewed the PR at HEAD The outstanding finding from the last round is resolved. Commit
Re-verified, no regressions from earlier rounds:
No new correctness, architecture, or maintainability issues found in this round's diff. Not independently verified (no network egress in this environment): the actual |
…scope (#2191) No linked issue ## Summary `upstream-drift` 1.2.0 shipped the fetch route yesterday ([#2185](#2185)). [#2187](#2187) applied it across 327 stamps today and surfaced **two holes in it**. Both are the route's own failure mode — a false absence carrying every outward sign of a good read — one scope up from where 1.2.0 drew the line. This is the convention's recheck discipline finding gaps in the convention, one release after it shipped. `upstream-drift` 1.2.0 → 1.3.0, additive; no required part, canonical name, or enforceability verdict changed. Pure `docs/`, so no plugin bump. ## Fix ### Hole 1 — a `200` does not mean you got the page you asked for 1.2.0's rung 1 guarded against truncation and against a channel that 404s. It did not guard against a channel that **succeeds with the wrong page**. A retired slug is silently aliased to its successor — no redirect, no `Location`, no notice in the body: | Probe | Result | |---|---| | `slash-commands.md` | `200`, `text/markdown`, 82,668 bytes, first heading `# Extend Claude with skills` | | `skills.md` | `200`, `text/markdown`, 82,668 bytes, same heading | | SHA-256 of both | `a833dd5c96b9b111de0daec5fc6436e210c8cdc009e51306d32438746db0b5a5` — **byte-identical** | | Rendered `slash-commands` | `200`, `num_redirects: 0` | | `nonexistent-page-xyz.md` | `404` — so this is **not** a catch-all; aliasing is specific to slugs that once existed | **This outranks truncation as a failure.** Truncation at least yields text you can see is short. Here a search for a term the *requested* page owns comes back empty against a full, healthy-looking body. Identity is therefore now part of rung 1, with two cheap checks: - **Confirm the slug against `llms.txt`.** Verified across ten slugs: the nine live ones each appear as `docs/en/<slug>.md`; `slash-commands` appears in no such entry (only an unrelated `agent-sdk/slash-commands`) — exactly the one that aliased. A mechanical detector, not a judgment call. - **Read the body's first heading before quoting it.** A heading that does not match the page you asked for ends the read. A title merely *worded* differently from the slug does not — `sub-agents.md` is titled "Create custom subagents", `costs.md` "Manage costs effectively"; both are correct pages. A missing slug is not a dead end — it is a prompt to find the successor in the index and cite **that** slug. Left unchecked, a citation of a retired slug keeps working indefinitely while pointing somewhere its author never read, then becomes a `404` on a claim nobody re-derived the day the alias is dropped. ### Hole 2 — an absence claim now carries its scope 1.2.0 said a truncated read supports no absence claim. It never said a **complete** read of one page supports no claim about the product. Two moves break it: - **Widening the subject.** Searching `hooks` and concluding "Claude Code has no X" asserts something about every page not searched. The honest form names the corpus: "not documented on `hooks`" — or, if the sweep genuinely covered the index, "not documented on any page listed in `llms.txt` as of `<date>`", a far larger and more expensive claim. - **Searching the phrase instead of the capability.** Verified on `hooks.md`: the phrase "verbose hooks" appears **zero** times, while the same page documents > Async hook completion notifications are suppressed by default. To see them, enable verbose mode > with `Ctrl+O` or start Claude Code with `--verbose`. and separately > set `CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose` to see additional log lines such as hook matcher > counts and query matching A phrase search returns nothing here and licenses "no verbose hooks toggle exists" — false, from a complete, untruncated read of the *right* page. Stated as its own rule because it is the reason to care: **a sound conclusion resting on a false premise is fragile, not safe.** The instance above kept its conclusion on a corrected premise ([#2190](#2190)); the next reader who checks a false premise discards the conclusion with it. Fix the premise and keep the conclusion — never keep a premise because the conclusion it props up is convenient. ## Verification Every claim above was re-derived here directly rather than taken on report, per the rule this section states — a report of an absence is exactly the thing the convention says not to accept second-hand: - Both `.md` bodies fetched and hashed locally; `slash-commands`/`skills` identity confirmed by matching SHA-256, byte count, and first heading - Redirect behavior probed with `curl -L -w '%{num_redirects}'` on the rendered URL - `404` control run against an invented slug, establishing the alias is not a catch-all - `llms.txt` fetched (187 `docs/en/` entries) and checked slug-by-slug across all ten - `hooks.md` fetched in full; `grep -ic "verbose hooks"` → `0`, and all four `verbose` mentions read verbatim - Nine live pages fetched to confirm the title-vs-slug check does not produce false positives **Gates (committed tree, CI form):** `check-contract-slice-prune.sh --check-diff origin/main`, `check-changelog-parity.sh --check-bump origin/main`, `check-skill-portability.sh`, `check-shell-portability.sh` — all pass; `markdownlint-cli2` over both changed files — 0 errors. Remaining gates: CI is the authority. ## Related - [#2185](#2185) — shipped the fetch route this patches - [#2187](#2187) — applied it at scale and found both holes - [#2190](#2190) — withdrew the over-scoped nonexistence claim that hole 2 generalizes --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…searched (#2190) No linked issue ## Summary PR #2187 claimed, as a headline result, that reading complete raw pages made two negative claims assertable "for the first time". **One of them was wrong**, and wrong in the way negative claims usually are: I searched one page and stated the result about the product. This corrects it. The rule it supports does not change; its justification does. ## Fix ### The claim that was wrong `docs/conventions/hook-observability/README.md` read: > **Not a UI feature.** No native "verbose hooks" toggle exists in Claude Code as of 2026-08-10 (confirmed against the same fresh fetch this doc cites) The literal phrase "verbose hooks" does appear on no page. But `verbose` appears across **at least 13 Claude Code docs pages**, four of those mentions on `hooks` itself — and one is squarely on point: > Async hook completion notifications are suppressed by default. To see them, enable verbose mode with `Ctrl+O` or start Claude Code with `--verbose`. A verbose mode that reveals hook output is precisely what the bullet denied. Also present, none of it acknowledged: | Surface | What it is | | :-- | :-- | | `verbose` setting | "Show full tool output instead of truncated summaries" | | `viewMode` setting | `"default"` / `"verbose"` / `"focus"` | | `--verbose` flag | Full turn-by-turn output; overrides `viewMode` | | `CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose` | Hook matcher counts and query matching | | `--include-hook-events` | Hook lifecycle events in the stream-json feed | ### Why the rule still stands None of those is a **consumer-facing toggle that makes an ordinary hook's routine work visible**. Each is operator-driven debugging, a transcript view the consumer must already have switched on, or a machine feed for a `-p` harness. A plugin cannot depend on an operator's debug posture, and none of them changes where a hook must *put* its message. So `statusMessage` and `systemMessage` remain the surfaces a fleet hook writes to, and no hook in the fleet changes. What changes is that the rule now rests on "a plugin cannot assume an operator's debug posture" instead of on a nonexistence claim that was false. A correct conclusion resting on a false premise is fragile — the next person to check the premise has reason to discard the conclusion with it. ### Second fix, same class The same file attributed this to the hooks page: > The harness's own signal for it is a generic "PostToolUse hook modified `<file>` after your edit (likely a formatter)" line That string appears on **no** Claude Code docs page. It is an observed harness string, and the sentence read as though it had been verified against the page cited beside it. It is now labelled as observed, and the documented negative it sits next to — that the three output channels carry no file-change or diff surface — is kept and separately attributed, since that is the part the rule actually needs and it does hold. ## Verification - `verbose` occurrence counts taken across the full raw-markdown corpus fetched via the rung-1 route in [`upstream-drift`, "Reading the basis"](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/upstream-drift/README.md#reading-the-basis--the-fetch-route): 13 pages carry it, `hooks.md` four times. - The `Ctrl+O` / `--verbose` sentence quoted verbatim from `hooks.md`. - "PostToolUse hook modified" and "likely a formatter" both return zero matches across every page in the corpus. Gates: `check-contract-slice-prune` pass · `check-changelog-parity --check-bump` pass · `check-skill-portability` pass (no skill files in scope) · `markdownlint-cli2` 0 errors. Pure `docs/` change — no plugin version bump required. ## Related - PR #2187 — introduced the claim this corrects, as one of its two advertised negatives - PR #2185 — established the rung-1 fetch route; its gotcha "absence from one page is not absence" is what prompted re-checking my own merged work, and is what caught this --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ill a real hook inventory (#2301) ## Summary Two clusters in `claude-config:audit`, both of the same shape: the skill claiming coverage it did not have. **Category G was unexecutable (#2274).** It is the one Phase 2 category a consumer could not run. - Its only overflow detector was `/doctor`, which needs an interactive TTY, so the category yielded nothing in the harness's own headless mode. It now names the documented headless route — *"When the listing exceeds its budget, Claude Code also writes a warning to the debug log, visible with `--debug`"* — and reports which route it took. `/context`'s Skills row is labelled as what it is: a second *interactive* reading, not a headless substitute. - It stated no budget constant, so "overflowed, and by how much?" was unanswerable by hand. It now carries `skillListingBudgetFraction` default `0.01`, `SLASH_COMMAND_TOOL_CHAR_BUDGET`'s documented 8,000-character fallback, `skillListingMaxDescChars` default `1536`, and the `200,000 × 4 × 0.01 = 8,000` arithmetic reconciling the first two. - Its "cheapest first" lever list put `skillOverrides` second while carrying the caveat that it does not reach plugin skills, and never named the substitute upstream prescribes — so on a plugin-heavy roster the ordered list degenerated to the entry it labels "last resort". Levers are now split by roster origin, `/plugin` is named, and a roster-composition count is required before any lever is recommended. No per-skill `name-only` state is promised for `/plugin`; no page documents one. - It was the only letter with no checklist table (headings ran A–F, **H**, I). It has one now, and the sentence at `audit-checklist.md:5-7` that said it had none is gone rather than left contradicting the same commit. **Corrected remedy for the table row.** The originating item asked for a new `check-skill-listing-budget.sh`. That measurement already exists as `plugins/skill-quality/scripts/check-listing-budget.sh` (#1404), so the fix is wiring — and the wiring says out loud what the item did not: **the two tools measure different populations.** That script walks a *repository's* skills roots; Category G asks about the listing a *consumer's session* assembled. It is pointed at as an explicit in-repo proxy, never as the consumer's number. Its measured cost is stated too (below), because a marketplace-wide invocation cannot finish inside a default Bash tool timeout (#2216). **`audit` asserted hook coverage it could not enumerate (#2275).** Category D writes rules for `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` placeholders that appear only in a plugin-provided hook, and Category B's third baseline narrowing turns on whether such a hook is live — while three of the skill's own surfaces disclosed in prose that the enumeration was impossible (`context/procedures.md`, `reference/required-permissions.md` twice). Gap disclosed in three places, closed in none. New `scripts/check-hook-coverage.sh` (+ `.test.sh`) closes it. It resolves each enabled plugin through the installed-plugin registry — every record carries a version-pinned `installPath`, so no version-directory ordering is inferred — and reads its hook config in all four documented shapes (`hooks/hooks.json`; a `hooks` path, array of paths, or inline object in `plugin.json`; plugins-reference: *"Location: `hooks/hooks.json` in plugin root, or inline in plugin.json"*, key typed `string|array|object`). It also reports `disableAllHooks` / `allowManagedHooksOnly` / `strictPluginOnlyCustomization`, because a hook a setting has switched off is not coverage. The **exit code is the contract**: `0` complete, `1` partial with the unenumerated sources named, `2` no inventory. The fail-open posture is kept and narrowed — it applies to what a partial run could not read, not to every run by default. This also removes the ordering problem the narrowing-3 precondition used to carry: Phase 1.0 runs before Category B, so the lever reading is available even on a scope-filtered `/audit permissions`. All three "no enumeration path exists" surfaces were rewritten in the same commit, and the eval that asserted "having no way to enumerate a plugin's hooks/hooks.json" was rewritten with them. **A live defect found while writing it, with the mechanism stated correctly.** On Git for Windows `jq` writes stdout in **text mode** and appends a CR to every line — a property of jq's own output stream, **not** of the input file's line endings. Untreated, a plugin key read out of `jq` is `name@marketplace\r`, every registry lookup misses, and the plugin is reported as not installed on a machine where it is installed. My first comment blamed CRLF input files; the red run refuted that (LF-only fixtures failed too) and the comment was corrected to match. Pinned by a regression case. **Also in this PR:** - **`destructive-bash-deny` ships with its fragility.** Eight argument-constraining globs, rated `error` when absent, with no caveat — in a file that quotes the permissions page's *"Bash permission patterns that try to constrain command arguments are fragile"* one section earlier, against a different table. The concrete hole is now stated: matching is prefix-anchored, so `Bash(git push --force *)` does not match `git push origin main --force`. The patterns stay — they raise the cost of an accidental force push — but a finding no longer implies they bound a determined one. Deliberately **not** claimed: that upstream ranks the hook above the glob for destructive commands. That page's remedy recommendation is scoped to URL filtering; only the fragility half is doc-supported, and the PR says so. - **The "speed bump, not a boundary" hook ranking is scoped, not deleted.** Correct for secret exfiltration, where an OS boundary exists. Stated unscoped, it also governed destructive git — where the sandbox's vocabulary is `filesystem.*` paths and `network.*` hosts, with no expression for a command's arguments, so it cannot separate `git push` from `git push --force` to the same remote. - **The read-it-verbatim guard covers all of Phase 3**, not the single Category F row #2185 landed it on — and `settings`, fetched in Phase 3.1, is the page a summarizing fetch already reported three present keys as absent on. Every doc quote above was re-fetched as raw markdown (`curl …/docs/en/<page>.md`) on 2026-08-12 and grepped, not recalled. ## Test plan New suite, red-then-green. Red proof: deleting the CR strip from `jqs()`: ``` $ bash plugins/claude-config/skills/audit/scripts/check-hook-coverage.test.sh FAIL: case 1: complete inventory exits 0 FAIL: case 1: plugin hook enumerated FAIL: case 4: CRLF settings still complete FAIL: case 4: CRLF plugin resolved FAIL: case 5: inline hook enumerated FAIL: case 6: custom-path hook enumerated FAIL: case 9: inventory verdict present 17/34 checks failed. ``` Green, with the fix restored: ``` $ bash plugins/claude-config/skills/audit/scripts/check-hook-coverage.test.sh PASS: case 1: plugin hook enumerated PASS: case 3: partial inventory exits 1 PASS: case 3: never claims completeness PASS: case 4: CRLF plugin resolved PASS: case 5: inline hook enumerated PASS: case 6: custom-path hook enumerated PASS: case 7: missing declared path is partial PASS: case 8: lever reported PASS: case 9: --json output is valid JSON PASS: case 10: exit 2 with no readable scope PASS: case 11: exit 2 when jq missing All 34 checks passed. ``` Against the real machine (73 enabled plugins), the inventory the skill previously could not take: ``` $ bash plugins/claude-config/skills/audit/scripts/check-hook-coverage.sh Hooks (44): SOURCE EVENT MATCHER COMMAND settings:user PreToolUse Bash|PowerShell bash ~/.claude/hooks/block-destructive-removal.sh plugin:guardrails@melodic-software PreToolUse Bash|PowerShell "${CLAUDE_PLUGIN_ROOT}"/hooks/block-dangerous-git.sh … INVENTORY: complete — every enabled plugin resolved and every hook source parsed. ``` The Category G proxy-cost figure quoted in the checklist, measured here: ``` $ time bash plugins/skill-quality/scripts/check-listing-budget.sh plugins/claude-config/skills Shared listing-budget estimate over 8 listing-eligible skill(s) across 1 root(s): aggregate: 5892 chars budget: 8000 chars (documented default (SLASH_COMMAND_TOOL_CHAR_BUDGET fallback)) CHECK-LISTING-BUDGET: OK — aggregate 5892/8000 chars within budget. real 0m5.983s ``` Repo gates: ``` $ shellcheck --rcfile=.shellcheckrc -x …/check-hook-coverage.sh …/check-hook-coverage.test.sh SHELLCHECK CLEAN $ bash scripts/check-shell-portability.sh --paths …/check-hook-coverage.sh …/check-hook-coverage.test.sh No unexcused GNU-only constructs in 2 shell file(s). $ CHECK_SKILL_SKILLS_ROOT=plugins/claude-config/skills bash plugins/skill-quality/scripts/check-skill.sh audit INFO: markdownlint clean INFO: script test passed: scripts/check-hook-coverage.test.sh INFO: script test passed: scripts/check-plugin-drift.test.sh INFO: script test passed: scripts/check-structure.test.sh INFO: script test passed: scripts/fix-plugin-drift.test.sh CHECK-SKILL audit: PASS — 0 errors, 2 warning(s) $ bash plugins/skill-quality/scripts/check-evals-quality.sh plugins/claude-config/skills/audit/evals/evals.json check-evals-quality: PASS (0 warning(s) across 1 file(s)) $ npx markdownlint-cli2 plugins/claude-config/CHANGELOG.md "plugins/claude-config/skills/audit/**/*.md" Summary: 0 issues in 0 files $ bash scripts/check-changelog-parity.sh --check && --check-order && --check-bump origin/main Every versioned plugin has a CHANGELOG.md … All 75 changelog(s) read newest-first with no duplicate versions. Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry. ``` The two `check-skill.sh` warnings (SKILL.md over the 200-line soft target; no Gotchas surface) are pre-existing on this skill, not introduced here. ## Security review note `check-hook-coverage.sh` is a new **read-only** surface. It widens what the skill reads — the user-scope `settings.json`, the installed-plugin registry, and each enabled plugin's hook config — and adds no write, no network call, and no execution. It **never runs a hook command**; hook commands are printed as strings only. It grants nothing: no permission rule, no `allowed-tools` entry, no new hook. The one judgment it deliberately does not make is whether an enumerated hook *covers* a permission family; that stays with Category B against the three preconditions in `required-permissions.md`, so the new data cannot by itself downgrade a security finding. `plugin.json` bumped 0.33.0 → **0.34.0** (new surface + behavior change) with a matching CHANGELOG entry. ## Related Closes #2274 Closes #2275 Inbox items: `20260810-225906-claude-config-category-g-unmeasurable` (#2274), `20260810-225905-claude-config-audit-category-b-hook-blindness` (#2275). Ledgers: `.work/handoff-inbox-batch-4/ledgers/I3-claude-config-category-g.md` § B1–B5 · `.../I2-claude-config-category-b.md` § A4, A2, MN1. Adjacent, not duplicated: #1404 (closed) shipped the measurement engine Category G now points at; #2216 owns its Windows runtime; #2023 / #2034 / #1271 own the marketplace-wide budget program, and no repo-wide aggregate is quoted here on purpose. #1598 is A2's precedent on the Read deny table in the same file. #2185 landed MN1's partial fix (Category F only). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
No linked issue
Summary
https://code.claude.com/docs/en/env-varscould not be read whole. Three independent fetchestruncated before the
CLAUDE_CODE_MAX_*range, and the summarizer then reported those rowsabsent — a false negative indistinguishable from real drift (#2182).
This repo cites
env-varsin ~13 places, so that defect was standing, silent, and reproducible ondemand: every future claim about the page had the same failure waiting in it.
This is a capability gap, not a correctness one. The fix is a route that reads the page verbatim,
documented where the concern already lives, plus a re-derivation of every
env-varsclaim in therepo from that read.
Fix
The route, verified empirically
curlthe raw-markdown channel (https://code.claude.com/docs/en/<slug>.md) to a file and searchthe file. Against
env-varson 2026-08-10:200,text/markdown; charset=utf-8CLAUDE_CODE_MAX_*at lines 278–28643a805b4cfffd9aae5e36cec42f3a271dc92ddead26db76cd401d61ff4048584Last-Modified: Mon, 10 Aug 2026 23:53:10 GMTequalsDate, independently re-confirming the header finding already recorded atdocs/conventions/upstream-drift/README.md§Drift signalThe route is hoisted, not invented — from two surfaces that derived it independently.
plugins/claude-ops/skills/changelog/context/read-actions.mdcarried it page-scoped: "curlthe.mdand slice locally … Never report a version 'absent from the changelog' on a truncated fetch"./knowledge:docpage-digest's Anthropic publisher profile carried it claim-scoped, binding anyabsence-establishing fetch to
curlon the raw.mdchannel with a recorded length, on theasymmetry that "a truncated fetch cannot fabricate a PRESENCE, only an ABSENCE" — written after two
of its own runs asserted a false absence exactly this way. Two independent derivations of one rule
is the signal it wants an owner. Per the convention registry's one-owner-per-concern rule the
general form belongs in
upstream-drift; both surfaces keep their scope-specific detail, andnothing is copied in either direction. Same move #2182 made with the mirror, one level up.
The convention (
upstream-drift1.1.0 → 1.2.0)New §Reading the basis — the fetch route. Additive guidance only: no required part, canonical
name, or enforceability verdict changed.
supports no absence claim, ever — "not in the response" is never "not on the page", because the
reader cannot tell those apart. That is the whole failure being closed: a summarizing fetch does
not merely fail, it manufactures an absence that reads exactly like a real one.
curlthe.mdchannel (default). 2 — a summarizing fetch, admissible onlywhen the read shows the page arrived whole. 3 — a verbatim mirror, one rung below primary, and
the record says so, carrying fix(discipline): close the open concurrency-variable currency question in sweep-all #2182's freshness-corroboration step generalized: corroborate
against a fact the page's own content can only carry after a known upstream change, never
against the mirror's self-reported sync time.
.mdchannel is per-page.docpage-digest's existing warning — a raw-markdown channelthat works for one doc can 404 for another — is carried across as the reason a run verifies the
channel before trusting the rung.
content date. Anything stronger is the overclaim the doc's own first rule forbids.
The claims — 2 drifted, 1 citation rotted, 1 basis retired
claude-config0.29.0 → 0.29.1audit's MANDATORY env-var check instructed auditors to do the exact thing that fabricatesfindings. Category F said fetch the page and search it, calling it "the authoritative source",
with no word on how to read it. A summarizing fetch truncates 315 rows and reports the rest
absent — so an auditor following the row as written could flag a valid variable as unrecognized
and never know. Now routes through the
.mdroute and states that a truncated read supports nofinding at all. This is the capability gap made concrete, and it was live guidance.
The same row invited the inverse error. "Authoritative source" + "do not flag as unrecognized
without checking this page" reads as absent here means not real. It is not:
CLAUDE_CODE_ENTRYPOINT,CLAUDE_CODE_ENHANCED_TELEMETRY_BETA, andCLAUDE_CODE_EXPERIMENTAL_OBSERVER_AGENTSare each cited as real in this repo and each absentfrom the full verbatim read. The row now caps the verdict at "not documented on
env-vars" andnames the sibling pages to check.
audit-passcarriedDISABLE_DOCTOR_COMMANDas undocumented. It is documented — anddescribes that very skill:
It moves into the verified list with the scope the row actually draws (session skill, not the
terminal command), and independently corroborates the v2.1.205 cutover the same section already
states. The
skillOverrideshalf is untouched and still says UNVERIFIED — this run re-derivedthe
env-varsbasis only, and the trigger now names thesettingsfetch that would retire it.audit-instructions' effort-audit reading list promised a release the page does not state. Itsent auditors to
env-varsforCLAUDE_CODE_DISABLE_ADAPTIVE_THINKING"with the models andrelease it reaches". The row states the models — "Has no effect on Fable 5, Sonnet 5, or Opus
4.7 and later" — and no release at all. Telling a reader to go find something that is not there
invites them to invent it. Corrected, and routed through the fetch route for the same reason.
discipline0.12.2 → 0.12.3 — a trigger written yesterday fired today. #2182 sourcedCLAUDE_CODE_MAX_TOOL_USE_CONCURRENCYfrom a mirror and wrote its own retirement condition: "anyenv-vars fetch that reaches the
CLAUDE_CODE_MAX_*range, which retires the mirror basis for aprimary one." The rung-1 fetch reached it. The row is unchanged —
— so no value moves; what changes is the citation's standing, mirror → primary. Four more rows the
preflight leans on were read in the same fetch and each matches how it cites them:
CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION("Removed in v2.1.224 and now a no-op … Previously capped …(default: 200)"),
CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS("default: 20"),CLAUDE_CODE_DISABLE_BACKGROUND_TASKS("therun_in_backgroundparameter on Bash and subagenttools"),
CLAUDE_CODE_FORK_SUBAGENT("overriding any server-side rollout").knowledge0.11.0 → 0.11.1 — a line-number citation into a live page had rotted. (Its secondchange is a one-sentence provenance pointer: the profile's absence-fetch rule is named as rung 1 of
the route it was generalized from. The rule and its asymmetry are unchanged and stay where they
are.) The Anthropic
profile recorded its sole attested
api-onlynear-miss sub-shape (3) asenv-vars.md:394. On theverbatim read, line 394 is
DISABLE_UPGRADE_COMMAND. The row the instance actually describes isFALLBACK_FOR_ALL_PRIMARY_MODELS— the only row on the page that both describes Claude Code's ownretry behavior and names a model subject ("models Claude Code recognizes as Opus, Fable 5, or
Mythos models stop retrying this way"); the sibling
CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACKnames none, so it is excluded. The attestation stands; only its address moved. Cited by variable
name now, with a standing rule: cite a live docs page by anchor or row key, never by line number.
Line numbers into an archived snapshot stay citable — that file is immutable, which is what makes
them citable.
playbooks0.8.2 → 0.8.3 — verified current, with one qualifier honestly downgraded.sonnet-5.md's harness-side thinking facts re-verify:MAX_THINKING_TOKENSstill carries theAnthropic-API-vs-third-party split, the Fable 5 exception, and the nonzero-ignored rule, and
CLAUDE_CODE_DISABLE_ADAPTIVE_THINKINGnow states the file's central claim outright — "Has noeffect on Fable 5, Sonnet 5, or Opus 4.7 and later". The "from Claude Code v2.1.111" attribution did
not re-verify: the page states no release for that variable. Flagged in place rather than
deleted or quietly kept — uncontradicted and immaterial, but a reader is owed the difference between
a claim re-read today and one carried forward.
docs/OFFICIAL-DOCS.md— theenv-varsrow's stamp moves 2026-08-06 → 2026-08-10, and thestaleness warning now points at the fetch route, because this file's whole job is carrying per-page
verification dates. Other no-drift stamps were deliberately not refreshed (the two
statusline-shim.shCLAUDE_CONFIG_DIRquotes, verified current below): a date refresh with noverdict change would cost two plugin bumps for zero information, and the convention's
read-time-validation rule says a lookup finding no drift obliges no edit.
Verification
Every
env-varsclaim in the repo, re-derived from the 2026-08-10 verbatim read. 13 citationsites and 50 distinct variable names swept.
audit-checklist.mdCategory F (method defect, both directions);doctor-handoff.mdDISABLE_DOCTOR_COMMAND;criteria.md's "and release it reaches"anthropic-docs-profile.mdenv-vars.md:394sweep-allCLAUDE_CODE_MAX_TOOL_USE_CONCURRENCYsweep-all×4 more rows;statelessCLAUDE_CODE_DISABLE_AUTO_MEMORY(SKILL.md +official-guidance.md+ README);continue-in-backgroundCLAUDE_CODE_FORCE_SESSION_PERSISTENCE/CLAUDE_CODE_CHILD_SESSION; bothstatusline-shim.shCLAUDE_CONFIG_DIR;official-corroboration.md's four owned namessonnet-5.md"from v2.1.111" — page states no release for that variabledoctor-handoff.mdskillOverrides— asettingsclaim, out of this page's scope; not re-read, and the record says soOFFICIAL-DOCS.mdrow,criteria.mdreading list,CHANGELOGhistory (never rewritten)Non-Claude-Code names swept up by the variable grep —
BASH_SOURCE/BASH_REMATCH(shell builtins),PLAYWRIGHT_MCP_VIEWPORT_SIZE, and this repo's ownMCP_GATE_ENABLED/BASH_LINT_ENABLED— are outof scope: absence from
env-varsis expected and means nothing.Two representative verbatim quotes, both matching the repo as cited:
Gates (committed tree, CI form):
check-contract-slice-prune.sh --check-diff origin/main— pass, no path underdocs/topics/check-changelog-parity.sh --check-bump origin/main— pass, every bumped plugin has its entrycheck-skill-portability.sh origin/main— pass, 4 skill filescheck-shell-portability.sh origin/main— pass, no shell files in scopecheck-changed-skills.sh origin/main— passmarkdownlint-cli2over all 12 changed markdown files — 0 errorsvalidate-plugin-contracts.mjs(2241 files),generate-catalog.mjs --check,generate-cheatsheet.mjs --check— passReview round 1 (f2f05d8) — both findings real, both accepted:
claude: a 315-vs-318 row-count split across files citing the same read. Correct, and mine.grep -c '^| \'` returns 318 because the page has a second, three-row settings-file table; thevariables table is 315. I caught it pre-PR but the corrective sweep matched the phrase literally
and skipped two line-wrapped occurrences. On a PR arguing no-verbatim-quote-no-claim, an
unreconciled count is the defect it argues against.
chatgpt-codex-connector(P2): relative../../docs/links die on plugin install. True — themarketplace cache holds
plugins/<name>, not the repo-level docs tree. All in-plugin referencesare now absolute GitHub URLs, matching the form
plugins/planning/reference/topic-docs.mdalready uses. Vendoring the guidance instead was declined: five copies of the rule would violate
pointer-not-copy, which the upstream-drift doc's own Boundary defers to. The operational rule is
stated inline at every site regardless, so the link is provenance rather than a dependency. Two
pre-existing CHANGELOG links the rewrite caught were restored — history is never rewritten.
Reviewing that file surfaced prior art this PR had understated, which is why the hoist framing above
is stronger than it was at open: the profile already bound absence fetches to
curl-the-.md. Theduplicate paragraph was withdrawn, and the profile's own earlier read of this page became the
evidence for the line-numbers-rot rule — stated as line numbers, not row counts: it records
CLAUDE_CODE_MAX_OUTPUT_TOKENSat line 277 of a 451-line page, and that row is at line 280 of 458today. A row-count comparison was drafted and dropped: the earlier read's "316 rows" has no recorded
counting rule, and this PR just spent a round establishing that two defensible rules on this page
differ by three. Two line numbers from two records need no such assumption.
Related
one variable via a mirror; this generalizes its protocol and retires the mirror basis it wrote,
by the terms of the trigger it wrote. A convention firing correctly within a day is the argument
for writing retirement conditions down instead of leaving a rung permanent.
env-vars— the primary page, now readable in fullplugins/claude-ops/skills/changelog/context/read-actions.md— the page-scoped prior art hoisted