docs: migrate remaining docs.claude.com URLs to code.claude.com - #1567
Conversation
Anthropic moved the Claude Code documentation from docs.claude.com/en/docs/claude-code/<slug> to code.claude.com/docs/en/<slug>. The old URLs still 301-redirect, so this migrates while the redirect works rather than after it is retired. A repo-wide sweep found 369 code.claude.com references against 5 stale docs.claude.com ones — the bulk migration was already done. docs/OFFICIAL-DOCS.md, this repo's canonical index, was checked and is already fully migrated. Four of the five stragglers are rewritten here. The path shape changes, so each target was verified by fetching the old URL, observing the redirect, and confirming the destination page's topic — /settings, /quickstart and /troubleshooting each 301 cleanly. The bare docs root does not: it 302s to platform.claude.com then 307s to code.claude.com/docs, so it now points at the Overview page, matching this repo's dominant /docs/en/<slug> convention. The fifth, plugins/playbooks/skills/boris/vendor/SKILL.md, is deliberately left alone — it is the verbatim upstream baseline kept for drift detection, and any edit would manufacture false drift. Refs #1564 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 0s —— View job PR Review
This PR is clean and correct. Four URL rewrites, two patch version bumps, two matching changelog entries. No issues found. URL migrationsAll four rewritten URLs correctly apply the domain change (
Bare-root substitution rationale is sound. The bare Vendor baseline
Version bumpsBoth are patch bumps ( Changelog entriesBoth changelog entries are placed under Remaining stale referencesA post-merge sweep confirms only three
No action needed on any of them. No findings. LGTM. |
|
Claude finished @kyle-sexton's task in 1m 10s —— View job Security Review
No security issues found. Scope reviewed: 6 files — two Attack surface assessment: The changed files are documentation reference content. The URLs in Both the old domain ( The The CHANGELOG additions are plain prose — no executable content. |
Resolved plugins/claude-ops/CHANGELOG.md: both sides added a section under the same heading and both belong. #1567 landed a Fixed entry for the docs.claude.com migration and took 0.21.5 with it, so this branch's Changed entry moves to a new 0.21.6 and plugin.json follows — otherwise two unrelated changes ship under one version. The merge touches no file under scripts/, so the gate's own behaviour is unchanged by it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uj3rsit5cxxuJuCXd1Hz2E
…ze guidance (#1586) Closes #1575 Closes #1576 Closes #1577 ## The problem The `playwright` skill's canonical video example is a bare `playwright-cli video-start demo.webm`. That deterministically produces an **800x450** file. An operator following the skill verbatim — including setting a large viewport with `resize` — gets 800x450 recordings every time, and nothing in the skill explains why or how to change it. The downstream report was nine E2E screen recordings, all 800x450, after a 1920x1200 `resize`. ## Which fix is correct, and why (stated explicitly) The task framing offers two valid resolutions: make the guarantee true, or stop claiming it. **`--size` does exist upstream** — verified in `help.json` extracted from `@playwright/cli@0.1.17`, the exact version this skill's frontmatter pins: ```text Options: --size video frame size, e.g. "800x600". if not specified, the size of the recorded video will fit 800x800. ``` So the guarantee is deliverable, and this PR makes it true rather than retracting it. Worth being explicit about what "behavior" means for a skill: **this skill's behavior *is* the commands it instructs an agent to run.** Changing the canonical example to pass `--size` is therefore the behavior fix, not a prose paper-over of a broken one. There is no separate code path left unfixed. The one place I *did* choose retraction over repair is the cost figure — see defect 3. ## Verdicts on the three reported defects ### 1. `--size` undocumented — REAL (#1575) Confirmed three ways: the installed CLI's `--help`, `help.json` from the pinned 0.1.17, and an ffprobe-measured repro. The string `--size` appeared nowhere in the skill. ### 2. "Defaults (accept, don't override)" steers wrong — REAL, but NARROWER than reported The report framed this as the section contradicting the fix. On inspection it is more precisely an *omission*, and two sub-claims do not survive: - The `| Viewport | 1280x720 |` row is **factually correct** — that is the CLI's default viewport, per the `@playwright/cli@0.1.17` README (`PLAYWRIGHT_MCP_VIEWPORT_SIZE` — "specify browser viewport size in pixels, for example \"1280x720\""). It is not a defect and it stays. - The section says "don't add `PLAYWRIGHT_MCP_*` env vars **to project settings**". The video fix is a per-command env prefix on `open`, which that sentence does not forbid. Not a contradiction. What was genuinely missing is a documented exception: an agent reading "accept, don't override" next to a 1280x720 row reasonably concludes the viewport is not a knob to touch. So the fix here is one added carve-out paragraph with a cross-link — not the section rework the report suggested. Demolishing the section would be overreach the evidence does not support. ### 3. "1280x720 WebM is ~5 MB/minute" — REAL Two errors in one line. The CLI never emits 1280x720 by default, *and* the `~5 MB/minute` figure is unsourced — it appears in no upstream skill, no `@playwright/cli` README, and no official Playwright doc. **Deliberately removed rather than re-anchored to 800x450**: correcting only the resolution would relocate the fabrication instead of fixing it, and short clips of a static page cannot ground a replacement number. Replaced with a qualitative statement (size scales with frame area and on-screen motion). ## Evidence Empirical repro, ffprobe on the resulting `.webm`, `@playwright/cli` 0.1.14 on Windows: | Scenario | Measured | |---|---| | `open`, bare `video-start` | `vp8, 800, 450` | | `open`, `resize 1920 1200`, bare `video-start` | `vp8, 800, 450` — `resize` does not move it | | `PLAYWRIGHT_MCP_VIEWPORT_SIZE=1920x1200 open`, bare `video-start` | `vp8, 800, 500` | | `open`, `video-start --size "1920x1200"` | `vp8, 1920, 1200` — but a frame extracted at n=20 shows the 1280x720 render in the **top-left corner**, rest padded grey | | both levers, matched | `vp8, 1920, 1200`, correct | That last row refines the original report, which described it as an "upscaled 1280-wide render". It is not upscaled — it is letterboxed top-left, exactly as <https://playwright.dev/docs/videos> describes: *"The video of the viewport is placed in the top-left corner of the output video, scaled down to fit if necessary."* Sources, all fetched or executed this session: - `help.json` from `@playwright/cli@0.1.17` (npm tarball, the pinned version) — `--size` present, identical text to 0.1.14; `flags: { size: "string" }` - `@playwright/cli@0.1.17` README — `PLAYWRIGHT_MCP_VIEWPORT_SIZE` env var and format; the `.playwright/cli.config.json` schema showing `browser.contextOptions` and a top-level `saveVideo: { width, height }` - <https://playwright.dev/docs/api/class-browser#browser-new-context> — `recordVideo.size`: "If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450." - <https://playwright.dev/docs/videos> — "You may need to set the viewport size to match your desired video size." ## Repo-convention compliance - **Version**: `0.4.0` → `0.5.0`. Precedent in this plugin's own CHANGELOG: 0.3.1/0.3.2 were pure doc corrections → patch; 0.4.0 folded in newly-surfaced upstream commands and flags → minor. Surfacing `--size` matches the 0.4.0 pattern. - **Frontmatter untouched.** No listing description or trigger-keyword change was needed, so none was made. `check-skill.sh` confirms *"all 10 base-ref trigger phrase(s) preserved"*. - **`vendor/` untouched.** It is a verbatim upstream baseline for drift detection; editing it would make the next `update --check` report false drift. Upstream's own shipped skill omits `--size` too — that is an upstream gap, noted as reportable, not patched here. - **Repo-agnostic.** The reported fix used `1920x1200`, the reporting operator's personal preference. Not baked in — the docs use a neutral illustrative `1440x900` and teach the *pattern* (set viewport at `open` AND pass `--size`, matched), since the number is the consumer's call. - **Fresh-docs mandate.** Scope is contract surfaces; this change is confined to skill prose bodies with frontmatter untouched, so it falls outside that scope. The load-bearing claims are grounded in the pinned package's own `help.json` and README plus playwright.dev, all obtained this session and cited above. No stale `docs.claude.com` URLs exist under `plugins/playwright`. ## Checks run locally - `check-skill.sh playwright` — **PASS**, 0 errors (1 pre-existing advisory warning: no Gotchas surface) - `validate-plugins.sh` — all manifests + catalog pass - `check-changelog-parity.sh --check` and `--check-bump origin/main` — pass - `check-skill-portability.sh origin/main`, `check-contract-slice-prune.sh --check-diff origin/main`, `check-shell-portability.sh origin/main`, `generate-catalog.mjs --check` — pass ## Deliberately deferred - **A dedicated `## Gotchas` section.** The report suggested one for "resize does not affect video frame size". That fact now lives in two places a reader actually hits — the SKILL.md Defaults exception (the exact spot that previously steered wrong) and the measured-outcomes table. A separate section would duplicate it to silence an advisory warning that predates this PR. - **The `.playwright/cli.config.json` route.** The README schema shows `browser.contextOptions` (accepts `viewport`) and a top-level `saveVideo: { width, height }`, and confirms `saveVideo` governs whole-session auto-save — a different mechanism from on-demand `video-start`. Documented as such and explicitly flagged unverified, since I did not measure it. - **Reporting the `--size` omission upstream** to `microsoft/playwright-cli`. ## Not verified Official Playwright docs do **not** anywhere state that video frame size is fixed at context creation and immune to a later resize. I searched `docs/src/videos.md` and the `Page.setViewportSize` section of `docs/src/api/class-page.md` in full. The claim is therefore attributed to measurement in this repo's wording ("`resize` does not change the video frame size" — what ffprobe showed), never asserted as documented upstream behavior. ## Related - #1567 — migrated remaining `docs.claude.com` URLs to `code.claude.com` repo-wide. Merged before this branch was cut, so there was no collision. Verified independently that no stale `docs.claude.com` URL remains under `plugins/playwright`; this PR adds none. - Upstream `microsoft/playwright-cli` — its own shipped skill (`vendor/references/video-recording.md`) omits `--size` too. Not closed by this PR and not patched here, since `vendor/` is a verbatim drift-detection baseline; reportable upstream as follow-up. - No ADR or decision-log entry applies — this is a documentation-correctness fix inside one plugin, with no architecture or contract-surface decision attached. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ission baseline (#1599) Closes #1598. ## What was wrong `plugins/claude-config/skills/audit/reference/required-permissions.md` recommended a `sensitive-file-deny` baseline (`Read(./.env)`, `Read(./secrets/**)`, `Read(./.claude/settings.local.json)`, key/PEM/SSH patterns) as the remedy for protecting secrets, without ever saying what a `Read` deny reaches. A reader who followed it believed the file was protected. This is a security-documentation defect: the doc created confidence the mechanism does not deliver. ## What I verified — and what I refuted Fetched this session: [permissions](https://code.claude.com/docs/en/permissions#read-and-edit), [sandboxing](https://code.claude.com/docs/en/sandboxing), [tools reference](https://code.claude.com/docs/en/tools-reference#powershell-tool). **Refuted, in part.** The originating report framed this as `Read(...)` and `Bash(...)` being separate matcher namespaces, so that a `Read` deny leaves `cat`/`grep`/`head`/`sed` open. Upstream says otherwise: Read and Edit deny rules "apply to Claude's built-in file tools **and to file commands Claude Code recognizes in Bash, such as `cat`, `head`, `tail`, and `sed`**." The obvious `cat` fallback is blocked. **Confirmed.** They "don't apply to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself." That is the real gap and it matches the reported observation (an interpreter one-liner) exactly. **Also refuted: the report's own suggested fix.** It proposed a `PreToolUse` hook on `Bash|PowerShell` inspecting the command string as "the durable remedy". Upstream documents argument-constraining Bash patterns as fragile, and a hook reading the same command string inherits the same evasion surface. Shipping that as the answer would have relocated the false confidence rather than removing it, so the hook is ranked below the sandbox and explicitly called a speed bump, not a boundary. ## What landed - **New "Scope of a Read deny" subsection** in `required-permissions.md`: covered vs not covered; `Bash(cat *)`-style enumeration named a non-remedy; the sandbox (`sandbox.filesystem.denyRead`, `sandbox.credentials.files` `"mode": "deny"`) as the documented OS-level path **with its platform limit** — it does not run on native Windows, so it is unavailable in exactly the environment that produced the observation; a `PreToolUse` hook as best-effort; and the residual risk stated plainly — a deny glob cannot keep a secret from a session that has shell execution, so the durable control is keeping the secret out of the session's reach. - **Two facts flagged unverified rather than asserted**: whether PowerShell-tool reads (`Get-Content`, `type`) are covered at all — upstream scopes the coverage to commands "in Bash" and the tools reference lists `Read(...)` as applying to "Read, Grep, Glob, LSP", neither addressing PowerShell; and the full membership of the recognized-command set, which upstream gives with "such as", leaving `grep`, `jq`, `strings`, and redirects unconfirmed in both directions. - **`SKILL.md`**: Category B reports the baseline with that scope in both directions — a present baseline is not reported as proof the file is unreachable. - **`context/procedures.md`**: the skill's own `settings.local.json` recipes no longer imply they escape the recommended deny. The safety is in what gets emitted, not what gets opened — `check-structure.sh` reads from a subprocess and is safe because it emits counts only, while the supplemental `cat … | jq` recipes are blocked in a compliant project, correctly so. Routing around that with an interpreter one-liner is prohibited; the audit reports the file as not inspectable under the project's own rule. - **Eval 7** (`read-deny-scope-not-overstated`) covering the new reporting behavior. - Version `0.13.0` → `0.14.0` with a matching CHANGELOG entry. **The pattern table is unchanged.** It is the Category B presence check; changing rows would change what the audit flags. ## Deliberately not done - **No new hook in `claude-config`.** `guardrails` owns `PreToolUse` on `Bash|PowerShell`; its `secret-pattern-detection.sh` matches `Write|Edit|NotebookEdit` (it blocks *writing* secrets), and its shell-matched hooks are git/commit-scoped, so nothing there covers credential reads today. Whether `guardrails` should grow a credential-read matcher is a product call for that plugin, and per the analysis above a command-string hook would be a speed bump regardless — it does not belong in `claude-config`. - **No repo-wide sweep.** Only `required-permissions.md` states this recommendation; no other plugin and no `docs/**` page does. ## Related - #1567 — the repo-wide `docs.claude.com` → `code.claude.com` URL migration whose destination domain every citation added here uses. - Not closed by this PR: whether `guardrails` should grow a credential-read `PreToolUse` matcher. That is a product call for `guardrails`, argued against as a *boundary* in this PR's analysis; no issue filed. - Not closed by this PR: `audit-checklist.md` B.4 flags `:*` permission-rule syntax as "deprecated", but the current permissions page documents `Bash(ls:*)` as an equivalent trailing-wildcard form. Noticed while verifying this item, out of its scope, left for a separate change. ## Verification `check-changed-skills.sh` PASS (0 errors), `check-changelog-parity.sh --check-bump` pass, `generate-catalog.mjs --check` in sync, `validate-plugin-contracts.mjs` pass, `validate-plugins.sh` pass, markdownlint 0 issues, evals JSON parses. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…blocked (#1587) Closes #1563 ## Summary `hook::buffer_stdin` read the hook payload with `IFS= read -r -d '' -t "$read_timeout"`. On a pipe — which is exactly how Claude Code delivers hook stdin — bash consumes `read -d ''` one byte at a time, measured at roughly **32 KB/s** on Git Bash. The `stdin_read_timeout` bound (default 2s) was therefore a **~64 KB throughput ceiling**, not the stall detector it was written to be. Past that ceiling the read returned a truncated payload and rc 2: - **Seven fail-closed guardrails guards** — `hardcoded-path-check`, `secret-pattern-detection`, `block-no-verify`, `block-dangerous-git`, `block-hook-bypass`, `block-noncanonical-commit`, `block-convention-violation` — map rc 2 to `exit 2`, so a legitimate large write was **blocked**, with its content never scanned. Reported from the field: a full-file `Write` of an 844-line (~50 KB) document blocked repeatedly, forcing the author to write it in five chunks. - **Every other caller** uses `|| exit 0`, so on a large payload the formatter / audit / advisory hook **silently did not run at all**, with no diagnostic. ## Reproduction (before the fix) `hook::buffer_stdin` in isolation, file redirect vs. pipe: | payload | file redirect | pipe | | --- | --- | --- | | 1 KB | 176 ms | 221 ms | | 10 KB | 240 ms | 461 ms | | 50 KB | 428 ms | **1564 ms** — 78% of the bound | | 100 KB | 666 ms | **rc 2 — BLOCKED** | | 200 KB | 1396 ms | **rc 2 — BLOCKED** | End-to-end, a benign `Write` payload (nothing in the content is a violation) piped into the real `plugins/guardrails/hooks/hardcoded-path-check.sh` with `CLAUDE_PROJECT_DIR` set: ``` payload=20504 rc=0 elapsed_ms=1810 payload=50939 rc=0 elapsed_ms=2857 payload=101664 rc=2 elapsed_ms=2853 BLOCKED: hook stdin timed out before a complete JSON payload arrived. payload=203113 rc=2 elapsed_ms=3006 BLOCKED: hook stdin timed out before a complete JSON payload arrived. ``` The block is purely a function of payload size. A 50 KB source file JSON-escapes to well over 50 KB of payload, which is why the 50 KB field report and this 100 KB reproduction are the same defect — the threshold sits in the 50–100 KB range and moves with machine load. ## Fix Two changes together, because either alone leaves the bound meaning the wrong thing. **1. Chunk the read.** `read -N 65536` lets bash satisfy the read in blocks rather than byte-at-a-time. Measured: 50 KB drops from ~2100 ms to **~20 ms**, 200 KB from ~6800 ms to **~85 ms**. All four end-to-end payloads up to 200 KB now return rc 0. **2. Measure inactivity, not the read.** `read -t` is a deadline for the *whole* requested read, not an inactivity timer, so a producer making steady progress but slower than one chunk per window would still trip it. `read` assigns whatever it received even when it times out, so any byte counts as progress: the partial chunk is kept and the read continues. Only the absence of bytes for a whole `stdin_read_timeout` is a stall. **2b. Read the bound in four slices.** `read -t` reports only that its window expired, never *when* inside it the last byte arrived — so armed as one window, a stall is declared anywhere between one and **two** bounds after the pipe actually went quiet. Slicing caps that overshoot at a quarter-bound. That residual quarter is the honest limit of the mechanism, and it errs toward waiting, never toward declaring a live producer dead. Measured back to back at a 1.2 s bound: a partial-then-silent producer is declared stalled at **2012 ms** sliced vs **2728 ms** unsliced. A shell whose `read -t` rejects the fractional slice degrades to a count of 1 — exactly the unsliced behavior — rather than failing. **3. Stop reading once the buffer is already whole.** Otherwise the Win32 late-EOF case — payload complete, pipe simply never closed — waits out the rest of the bound for an EOF that is not coming. The loop now checks completeness before continuing. Some wait is the floor, since a held-open pipe is indistinguishable from a slow producer until a window expires. Measured at a 1.2 s bound: **719 ms** with the early stop vs **1909 ms** without. The stall verdict now comes from `read`'s own exit status — EOF returns 1, an exceeded `-t` returns >128 — instead of elapsed-time arithmetic, which deletes the `EPOCHREALTIME`/`awk` heuristic and two `awk` subprocesses per hook invocation. The trade this makes, stated in the comment: a producer trickling bytes indefinitely is never cut off here. That is deliberate — Claude Code's own default timeout for a `command` hook is **600 seconds** ([hooks reference](https://code.claude.com/docs/en/hooks), fetched this session), so the harness is the outer bound, and blocking a live producer is exactly the failure this function had. **4. Validate the configured timeout instead of passing it straight to `read`.** `stdin_read_timeout` is consumer-configurable and reached `read -t` directly. An unusable spec is a **silent disable**: `read` rejects it with rc 1, the loop reads rc 1 as EOF, the payload comes back empty, every caller skips, and a usage error prints on every hook invocation. Worse, `stdin_read_timeout=0` made the chunked loop **spin** — `read -t 0` returns success having consumed nothing. `hook::resolve_read_timeout` now falls back to the default, deciding acceptance by **probing the running shell** rather than a Bash version table (the upstream changelog does not date the introduction of fractional timeouts, so a version check would be a guess). Loop termination is additionally made structural: a successful read that consumed nothing breaks rather than continues. **5. Never feed a hook payload to jq through a here-string.** `jq -e . <<< "$buf"` delivers the string through a pipe that **bash fills itself** before exec'ing jq, so a payload at or above the pipe capacity — 65536 bytes here, exactly one read chunk — **blocks the shell forever**. Traced: 65536 hung indefinitely, 65000 returned immediately. Every such call now goes through `printf | jq`: `hook::json_complete`, `buffer_stdin`'s final check, **`hook::jq_field`**, and claude-ops' `skill-usage-expansion-audit`. `hook::jq_field` is the load-bearing one — it takes the whole buffered payload and is called by most hooks in the fleet, and the old throughput ceiling is precisely what kept payloads that large from ever reaching it. Removing the ceiling made this deadlock reachable, so this PR had to fix it. **Bash 3.2.** `read -N` is Bash 4.1+, and nine plugin READMEs document Bash 3.2+ support (macOS system bash), so the pre-4.1 path falls back to the delimiter read inside the same re-arming loop — the same guard and rationale as `plugins/context-guard/scripts/statusline-tee.sh`. The guard is split into its own predicate purely so that path stays reachable in tests: `BASH_VERSINFO` is readonly and cannot be shadowed. It is not a consumer seam; nothing reads it from the environment. ## The fail-closed posture is unchanged This was the constraint the change had to respect — a guard that silently permits an unscanned write is strictly worse than one that is annoying. Verified three ways against the real hook: ``` 1. small + violation : rc=2 Hardcoded machine-specific path(s) in …/docs/probe.md: 2. 200KB + trailing viol. : rc=2 Hardcoded machine-specific path(s) in …/docs/probe.md: 3. stalled pipe : rc=2 BLOCKED: hook stdin timed out before a complete JSON payload arrived. ``` Case 2 is the improvement that matters most: a violation sitting at the very **end** of a 200 KB payload is now genuinely *caught*, where before the whole write was swept into a content-blind block. Case 3 is the posture proof — a truncated payload on a pipe held open still exits 2. The jq completeness check is kept as the backstop, so the **Win32 late-EOF** case this function exists for — a complete payload on a pipe that never closes — still succeeds rather than blocking. ## Tests `lib/hook-utils.test.sh` gains five cases alongside the existing timeout test: - **18b** — a *complete* JSON payload on a pipe held open past the timeout returns rc 0 with the payload (the Win32 late-EOF contract), **and** settles in one window rather than two. That second assertion is by comparison, not against a wall-clock constant: it times the real function and a variant with the completeness predicate overridden to always fail (reproducing the pre-fix behavior) back to back on the same host, so runner load cancels out. It fails loudly if the harness returns no measurement on a host that has `EPOCHREALTIME`. - **18c** — a 256 KB payload on a real pipe at the **default** timeout returns rc 0 with the content intact, asserted on content rather than wall-clock so a loaded runner cannot flake it. If the byte-at-a-time read ever returns, this fails outright. - **18d** — a payload trickled one character per 100 ms against a 300 ms timeout (far too slow to fill a chunk in any window) returns rc 0 with the whole payload. This is the idle-bound regression test; against the first commit on this branch it returned **rc 2**. - **18d'** — that same trickle then going *silent* mid-payload still returns rc 2, so re-arming on progress did not quietly become "never time out". - **18e** — first asserts the guard override actually flips to the pre-4.1 branch (otherwise the cases would be vacuous — the first attempt at this test shadowed `BASH_VERSINFO`, which is readonly, and silently tested the modern path), then that the delimiter-read fallback buffers a 128 KB payload and still fails closed on a stalled pipe. - **18g** — a stall is declared near **one** bound, not two, asserted against a variant with the slice count forced to 1, and gated on a precondition check that the override actually engages. - **18f** — five unusable `stdin_read_timeout` values (`abc`, `0`, `-1`, `1e3`, empty) must each fall back to the default, still deliver the payload, and emit **nothing** on stderr; plus a positive case proving a valid non-default value is still honored rather than collapsed. The `0` case doubles as the loop-termination test — before the guard it hung, so a regression shows up as the suite never finishing. Existing Test 18 (stalled pipe → rc 2 + `BLOCKED:`) is unchanged and still passes. - **18h** — a 65536-byte held-open payload (exactly one read chunk) costs no more than a non-boundary one, preceded by an assertion that the fixtures are *exactly* 65536 and 65000 bytes. This is also the here-string-deadlock regression test: before the fix it hung outright. Local runs: `lib/hook-utils.test.sh` 114/114 (66 s), `hardcoded-path-check.test.sh` 72/72, `secret-pattern-detection.test.sh` 42/42, `markdown-format.test.sh` 112/112, `typos-format.test.sh` 75/75, `block-hook-bypass.test.sh` 203/203, `stale-path-verify.test.sh` 87/87, `lane-stop-gate.test.sh` 26/26. `shellcheck` clean, `check-shell-portability.sh` clean. `sync-hook-utils.sh --check` / `--check-bump`, `check-changelog-parity.sh --check` / `--check-bump` all pass. **Not run: a real Bash 3.2.** No 3.2 host was available here, so the fallback is verified by forcing the branch on a modern bash, not by executing under 3.2 itself. **Four tests on this branch were silently vacuous before being caught** — one shadowed the readonly `BASH_VERSINFO` and tested the modern path while claiming to test the 3.2 fallback; one bracketed a whole pipeline and measured the producer's `sleep`; one had a missing `;` in its override string, so it exercised the unmodified function; one generated a 59578-byte "65536-byte" fixture because it stripped newlines after truncating. Every timing, branch-forcing, and exact-size case now asserts its own precondition first, so a broken harness fails rather than passes. ## Portability lint Two `portability-ok:` annotations were added for **pre-existing** false positives that only surfaced because this change puts all 14 synced copies into the changed set: a bash glob bracket class `[\<\>]` matching literal `<`/`>`, and a literal `a\b` path fixture — neither is a GNU `\<`/`\b` regex construct. Annotated with the gate's own documented hatch rather than touching the synced linter. ## Secondary: `stdin_read_timeout` in guardrails' userConfig Guardrails' hooks already read `CLAUDE_PLUGIN_OPTION_STDIN_READ_TIMEOUT` through the shared library but never declared the option, so a consumer had no supported way to set it. `actionlint` and `claude-ops` both declare it, and `actionlint`'s setup skill records the convention explicitly ("hook plugins reusing the shared lib should declare it too"). Declaring it exposes the same knob here, documented in the guardrails README. Deliberately **not** claimed: that the harness materializes the `default` into the env var. The plugins reference describes `default` as "value used when the user provides nothing" and says all values "are exported to hook processes as `CLAUDE_PLUGIN_OPTION_<KEY>` environment variables" ([plugins-reference](https://code.claude.com/docs/en/plugins-reference), fetched this session), but that specific default-materialization path is not something this PR verified end-to-end. The effective default with nothing configured remains the shell-level `:-2` fallback inside `hook-utils.sh`, which is code-verifiable here. ## Scope `lib/hook-utils.sh` is synced into all 14 carrying plugins by `scripts/sync-hook-utils.sh`; the coupled `sync-hook-utils` / `changelog-parity` CI gates require every carrying plugin to bump and add a changelog entry, so all 14 are bumped (guardrails to a minor, `0.18.0`, for the new userConfig option; the rest patch). The supported Bash floor is **unchanged at 3.2**. (An earlier revision of this PR claimed the library already required 4.0+ because of `${var^}` in `hook::normalize_path` — that was wrong: those case operators sit behind an `OSTYPE` msys/cygwin branch that never executes on macOS. The 4.1-only `read -N` is now guarded rather than unconditional.) ## Related - Refs #1564 / #1567 — the other half of the same handoff item (docs domain migration), landed separately. This branch is rebased on top of it, which is why `claude-ops` lands on 0.21.6. - Explicitly **not** related to the previously-closed `hardcoded-path-check` fail-**open** scope item: this is the contrasting fail-**closed** observation, not confirmation of that one. ## Deliberately out of scope `hook::buffer_stdin` returns 1 (skip) when stdin hits **EOF** with a truncated payload — a genuinely short write rather than a stall. Arguably that should fail closed too, but it has not been observed firing and changing it is a separate behavior change. Left as-is. Worth knowing when reading the new control flow: a truncated payload that goes quiet and *then* closes reaches that EOF branch (rc 1) rather than the stall branch, if the close lands before the silence exceeds one full timeout window. The stall branch requires a window that delivers nothing — which is the Win32 late-EOF shape this guard is actually aimed at, and what Test 18 and the end-to-end stall case both exercise. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Closes #1564
Summary
Anthropic moved the Claude Code documentation from
docs.claude.com/en/docs/claude-code/<slug>tocode.claude.com/docs/en/<slug>. The old URLs still 301-redirect, so this migrates the repowhile the redirect works rather than after it is retired.
A repo-wide sweep (every file type, not just markdown) found 369
code.claude.comreferencesagainst 5 stale
docs.claude.comones — the bulk migration was already done. Four arerewritten here; the fifth is deliberately left alone.
docs/OFFICIAL-DOCS.md, this repo's canonical index of official pages, was checked and isalready fully migrated (51
code.claude.comhits, zero stale).What changed
plugins/claude-ops/skills/known-issues/context/issue-templates.mddocs.claude.com/en/docs/claude-codecode.claude.com/docs/en/overview…/claude-code/quickstartcode.claude.com/docs/en/quickstart…/claude-code/troubleshootingcode.claude.com/docs/en/troubleshootingplugins/playbooks/skills/boris/reference/autonomy.md…/claude-code/settingscode.claude.com/docs/en/settingsVersion bumps + changelog entries:
claude-ops0.21.4 → 0.21.5,playbooks0.5.1 → 0.5.2.Verification
The path shape changes (
/en/docs/claude-code/X→/docs/en/X), so a mechanical string swap isnot safe. Every rewritten URL was verified by fetching the old URL, observing where it landed, then
fetching the proposed target and confirming its topic:
/settings— old 301s tocode.claude.com/docs/en/settings; target is "Claude Code settings"./quickstart— old 301s tocode.claude.com/docs/en/quickstart; target is "Quickstart"./troubleshooting— old 301s tocode.claude.com/docs/en/troubleshooting; target is"Troubleshooting".
docs.claude.com/en/docs/claude-code302s to
platform.claude.com/docs/en/claude-code, then 307s tocode.claude.com/docs— adifferent host from the other three.
code.claude.com/docs/en/overviewis therefore aninferred target, fetched and confirmed independently to be the real Overview page, chosen
because it matches this repo's dominant
/docs/en/<slug>convention rather than the bare root.Deliberately not changed
plugins/playbooks/skills/boris/vendor/SKILL.md:1129carries the same stale URL. Its siblingskills/boris/SKILL.mddeclares it the "verbatim upstream baseline… for drift detection only", soit is byte-compared against upstream and any edit here would manufacture false drift.
Deferred (not in this PR)
lychee.tomllink-checks every markdown file including that vendored baseline, and itsexcludelist has no claude.com entry. When Anthropic retires the 301, the weekly advisory link-check lane
will start filing a tracking issue for a URL that cannot be fixed by editing the file. The remedy
is a
lychee.tomlexcludeentry (or anexclude_pathfor the borisvendor/directory) — butlychee.tomlis a tool config materialized frommelodic-software/standards, so perAGENTS.mdthat fix belongs upstream rather than as a local patch. Recorded on #1564.
Related
large writes). Independent change, separate PR.