Compare commits

..

28 Commits

Author SHA1 Message Date
Yeachan-Heo e5d2eb1423 docs(roadmap): add subagent lane event conformance gap 2026-05-22 21:31:15 +00:00
Yeachan-Heo d2018d7aee docs(roadmap): add worker replay receipt integrity gap 2026-05-22 21:01:16 +00:00
Yeachan-Heo 71f85541bd docs(roadmap): add mcp tool bridge registry drift gap 2026-05-22 20:31:24 +00:00
Yeachan-Heo 20c9d9d6c3 docs(roadmap): add oauth authorize extra param collision gap 2026-05-22 20:01:13 +00:00
Yeachan-Heo 46f3bff7ef docs(roadmap): add mcp stdio frame size cap gap 2026-05-22 19:31:27 +00:00
Yeachan-Heo d996b65d64 docs(roadmap): add plugin uninstall transaction gap 2026-05-22 19:01:43 +00:00
Yeachan-Heo 244bdb78fd docs(roadmap): add plugin degraded server reporting gap 2026-05-22 18:31:51 +00:00
Yeachan-Heo 1df41147e3 docs(roadmap): add mcp degraded missing tools gap 2026-05-22 18:01:37 +00:00
Yeachan-Heo f50625a04b docs(roadmap): add remote mcp unsupported runtime gap 2026-05-22 17:31:15 +00:00
Yeachan-Heo 106c243bd3 docs(roadmap): add managed proxy mcp auth gap 2026-05-22 17:01:25 +00:00
Yeachan-Heo a93f36d0b9 docs(roadmap): add prompt piped stdin cap gap 2026-05-22 16:30:55 +00:00
Yeachan-Heo a1728b2be8 docs(roadmap): add repl missing default timeout gap 2026-05-22 16:01:10 +00:00
Yeachan-Heo 9843ccfa28 docs(roadmap): add plugin registry sync race gap 2026-05-22 15:31:00 +00:00
Yeachan-Heo 8043090953 docs(roadmap): add replay armed stale error gap 2026-05-22 15:01:23 +00:00
Yeachan-Heo b0bca2ea4f docs(roadmap): add worker restart evidence scope gap 2026-05-22 14:30:51 +00:00
Yeachan-Heo ac0903362c docs(roadmap): add permission file traversal gap 2026-05-22 14:01:09 +00:00
Yeachan-Heo aa9efe5474 docs(roadmap): add table alignment render gap 2026-05-22 13:30:55 +00:00
Yeachan-Heo ccb319a3dc docs(roadmap): add link label styling gap 2026-05-22 13:00:52 +00:00
Yeachan-Heo 8c4b33d9b1 docs(roadmap): add indented fence streaming gap 2026-05-22 12:30:49 +00:00
Yeachan-Heo 9f762b26fa docs(roadmap): add stream text-tool ordering gap 2026-05-22 12:00:54 +00:00
Yeachan-Heo d8864ff151 docs(roadmap): add websearch base url boundary gap 2026-05-22 11:30:40 +00:00
Yeachan-Heo bff67c2b24 docs(roadmap): add webfetch redirect boundary gap 2026-05-22 11:00:48 +00:00
Yeachan-Heo 571b4c2cd1 docs(roadmap): add glob brace fanout gap 2026-05-22 10:30:50 +00:00
Yeachan-Heo ab5754a524 docs(roadmap): add grep heavy-dir traversal gap 2026-05-22 10:00:54 +00:00
Yeachan-Heo 819f67b01b docs(roadmap): add kimi prefix wire-model gap 2026-05-22 09:01:22 +00:00
Yeachan-Heo c1bb355691 docs(roadmap): add grep zero limit gap 2026-05-22 07:30:46 +00:00
Yeachan-Heo eb12c3d1ef docs(roadmap): add grep output mode validation gap 2026-05-22 07:00:48 +00:00
Yeachan-Heo ef67aa9f96 docs(roadmap): add silent grep skip gap 2026-05-22 06:30:54 +00:00
+58
View File
@@ -6693,3 +6693,61 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
569. **Read-only bash validation does not unwrap `/usr/bin/env` command prefixes, so `env FOO=bar rm file` is allowed even though the executed command is write/destructive** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 05:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@2bb5d49`. Active tmux sessions at probe time: `gajae-pr-323-fix-windows-host-path-safety2`, `omx-pr-2452-madmax-lock-timeout-review`; no active claw-code implementation session. Channel context also had new PR claw-code#3056, but this pinpoint is from local permission-validation inspection. Code inspection: `rust/crates/runtime/src/bash_validation.rs::extract_first_command` strips leading `KEY=value` assignments and `validate_read_only` has a special recursive path for `sudo`, but there is no equivalent handling for the common `env KEY=value <cmd>` launcher form. `env` itself appears in `SEMANTIC_READ_ONLY_COMMANDS`, and in read-only mode `validate_read_only("env FOO=bar rm -rf target", PermissionMode::ReadOnly)` sees first command `env`, does not recurse into the inner `rm`, finds no redirection/git state path, and returns `Allow`. The same bypass applies to `env PATH=/tmp cp a b`, `env -i sh -c 'rm file'`, and likely `/usr/bin/env` shebang-style invocations if the first token is a path. Existing tests cover bare env assignments like `FOO=bar ls -la`, and sudo-wrapped writes, but not `env`-wrapped writes. **Required fix shape:** (a) teach first-command extraction or read-only validation to recognize `env`/`/usr/bin/env` prefixes, skip env flags/assignments, and validate the inner command recursively; (b) block or warn if the inner command cannot be parsed safely (for example `env -i sh -c ...`) under read-only mode; (c) add regressions for `env FOO=bar rm -rf target`, `env PATH=/tmp cp a b`, `env -i npm install`, and safe `env FOO=bar grep x file`; (d) keep the existing `KEY=value cmd` behavior but cover quoted assignment values; (e) report the unwrapped inner command in the block reason so operators can see which payload violated read-only mode. **Why this matters:** permission validation must classify the command that will actually execute, not the wrapper binary. `env` is a routine launcher in scripts; allowing it as read-only lets agents hide write/package/destructive commands behind environment setup and produces false safety evidence. Source: gaebal-gajae dogfood response to Clawhip message `1507246662630903910` on 2026-05-22.
570. **`read_file` binary detection scans only the first 8192 bytes for NUL, so text-prefixed binaries can pass the binary gate and fail later as generic UTF-8 read errors** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 05:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@a49d8e8`. Active tmux session at probe time: `gajae-issue-324-review-gate-degradation`; no active claw-code implementation session. Channel context from Jobdori pointed at `is_binary_file` line 30. Code inspection confirmed `rust/crates/runtime/src/file_ops.rs::is_binary_file` opens the path, reads exactly one 8192-byte chunk, and returns true only if that first chunk contains NUL. `read_file` then calls `fs::read_to_string` over the whole file. A file with an 8KB text header followed by NUL/non-UTF8 binary payload is therefore not rejected by the explicit binary gate; it reaches `read_to_string` and surfaces as a generic invalid UTF-8/io error instead of the stable `InvalidData: file appears to be binary` contract. Existing coverage writes NUL at byte 0 (`rejects_binary_files`) and does not cover delayed-NUL or delayed-non-UTF8 payloads. **Required fix shape:** (a) make binary/text validation cover the entire allowed read range (bounded by `MAX_READ_SIZE`) or stream decode as UTF-8 while detecting NUL/non-text bytes; (b) map any delayed NUL/non-UTF8 failure to the same stable binary-file error kind/message used by the early gate; (c) add regressions with NUL at byte 8192+, non-UTF8 after an ASCII prefix, and valid large UTF-8 text controls; (d) avoid loading oversized files by preserving the metadata size gate before full scan/decode; (e) include byte offset/evidence in diagnostics only if it does not leak file contents. **Why this matters:** `read_file` should fail predictably on binary artifacts. A small text header is common in mixed/generated files, and letting those bypass the binary gate creates inconsistent errors that look like brittle UTF-8/tool failures rather than an intentional binary-read refusal. Source: gaebal-gajae dogfood response to Clawhip message `1507254212403138672` on 2026-05-22.
571. **`grep_search` silently drops unreadable or non-UTF8 files, so search results can look complete while binary/permission/encoding failures are hidden from the output contract** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 06:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@8632f90`. Active tmux session at probe time: `gajae-issue-326-backlog-zero-continuation`; no active claw-code implementation session. Channel context included Jobdori's glob-sort #576 finding, so this probe stayed in the adjacent file-ops search surface but chose a different contract gap. Code inspection: `rust/crates/runtime/src/file_ops.rs::grep_search_impl` iterates `collect_search_files`, applies workspace/filter checks, then does `let Ok(file_contents) = fs::read_to_string(&file_path) else { continue; };`. Any file that exists in the search set but cannot be decoded as UTF-8, is transiently unreadable, or hits another read error is silently skipped. The returned `GrepSearchOutput` has `num_files`, `filenames`, optional `content`, and `num_matches`, but no `skipped_files`, `read_errors`, `binary_files`, or `truncated_due_to_errors` field. This differs from `read_file`, which intentionally rejects binary files with a stable error, and it makes grep output appear authoritative even when a subset of candidate files was ignored. Existing grep tests cover happy-path content matches but not a directory containing one matching text file plus one unreadable/binary/non-UTF8 file. **Required fix shape:** (a) track skipped candidate files by reason (`binary_or_non_utf8`, `permission_denied`, `read_error`) without leaking file contents; (b) expose counts and optionally bounded path samples in `GrepSearchOutput`; (c) preserve best-effort search behavior if desired, but mark the result as partial/degraded when any candidate is skipped; (d) add regressions with delayed-non-UTF8/binary and permission-denied files proving the output reports skipped counts; (e) align binary/non-UTF8 classification with the `read_file` fix required by #570 so file tools share one text/binary contract. **Why this matters:** grep is an observability surface. If it silently ignores files, agents can conclude “no matches” or report an incomplete match set without any evidence that encoding or permission failures narrowed the search. Source: gaebal-gajae dogfood response to Clawhip message `1507269308277850223` on 2026-05-22.
572. **`grep_search` accepts unknown `output_mode` strings as filename-mode success, so typos like `contents` or `json` silently change the result contract instead of failing fast** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 07:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@ef67aa9`. Active tmux session at probe time: `gajae-pr-327-backlog-zero-review`; no active claw-code implementation session. Channel context included Jobdori's adjacent grep duplicate-context #577 finding; this probe stayed in `grep_search_impl` but checked argument-contract handling. Code inspection: `rust/crates/runtime/src/file_ops.rs::grep_search_impl` sets `output_mode = input.output_mode.clone().unwrap_or_else(|| "files_with_matches")`, then special-cases only `"count"` and `"content"`. Any other string falls through to the default filename-list path and is echoed back as `mode: Some(output_mode.clone())`, with `content: None` and `num_matches: None`. That means misspellings such as `output_mode:"contents"`, `"files_with_match"`, or unsupported values like `"json"` return a successful-looking response whose `mode` claims the unsupported value while the payload shape is actually files-with-matches. There is no enum validation, no `InvalidInput`, and no test for unsupported output modes. **Required fix shape:** (a) validate `output_mode` against an explicit enum (`files_with_matches`, `content`, `count`) before reading files; (b) return a typed `InvalidInput`/machine-readable error listing supported values for unknown modes; (c) make the returned `mode` always match the actual payload semantics; (d) add regressions for `contents`, `files_with_match`, and valid `content`/`count`/default behavior; (e) keep this aligned with CLI `--output-format` enum validation gaps so search/tool contracts fail fast on typos. **Why this matters:** grep output shape drives model/tool parsing. A typo should not silently downgrade from content/count mode into filename mode while preserving the bogus mode label; that creates false negative evidence and parser confusion with no visible error. Source: gaebal-gajae dogfood response to Clawhip message `1507276861917368421` on 2026-05-22.
573. **`grep_search` treats `head_limit:0` as unlimited, so callers cannot request an empty/page-metadata probe and may accidentally dump the full match set** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 07:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@eb12c3d`. Active tmux session at probe time: `gajae-pr-330-review-v2`; no active claw-code implementation session. Code inspection: `rust/crates/runtime/src/file_ops.rs::apply_limit` is used for grep filenames and content lines. It computes `explicit_limit = limit.unwrap_or(250)`, but if `explicit_limit == 0` it returns the full post-offset item vector with `appliedLimit: None` instead of truncating to zero or rejecting the value. Therefore a caller using `head_limit:0` as a common “no rows, just metadata/count/page preflight” convention gets every matching filename/content line after the offset, bypassing the default 250 cap and potentially injecting a large result into the model context. Existing grep tests pass `head_limit: Some(10)` and do not cover zero-limit semantics. This also makes `appliedLimit` misleading: an explicit limit was supplied, but the output reports no applied limit. **Required fix shape:** (a) define `head_limit` as a positive integer and reject zero with `InvalidInput`, or make zero return an empty result with `appliedLimit:0` consistently; (b) never let zero disable truncation unless a separately named `unlimited:true` escape hatch exists; (c) add regressions for `head_limit:0` in filename and content modes, positive limits, default 250 truncation, and offset-only behavior; (d) ensure `appliedLimit` reflects the caller-supplied limit when accepted; (e) document pagination semantics so wrappers do not accidentally turn metadata probes into full dumps. **Why this matters:** search pagination is a context-window safety control. A zero limit should be safe or invalid, not the one value that disables the cap and can flood the assistant with every match. Source: gaebal-gajae dogfood response to Clawhip message `1507284411995918427` on 2026-05-22.
574. **Kimi compatibility helpers only strip the `kimi/` routing prefix, so documented `dashscope/kimi-*` and `moonshot/kimi-*` slugs can leak provider prefixes onto the wire** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 09:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@c1bb355`. Active tmux session at probe time: `omx-issue-2462-madmax-lock-diagnostic`; no active claw-code implementation session. Channel context included Jobdori's reasoning-history #581 finding in `openai_compat.rs`; this probe inspected the same provider file for another Kimi/OpenAI-compatible model-routing contract gap. Code inspection: `model_rejects_is_error_field` intentionally recognizes `dashscope/kimi-k2.5` and `moonshot/kimi-k2.5` by stripping any prefix with `rsplit('/')`, and tests assert those slugs reject `is_error`. But `wire_model_for_base_url` / `strip_routing_prefix` only strip prefixes matching `openai|xai|grok|qwen|kimi`; `dashscope` and `moonshot` are not included. Therefore a request model like `dashscope/kimi-k2.5` can be treated as Kimi for tool-result compatibility while the serialized `model` sent to a DashScope/Moonshot-compatible endpoint remains `dashscope/kimi-k2.5` instead of the expected `kimi-k2.5`. Existing tests cover `strip_routing_prefix("kimi/kimi-k2.5")` but not `dashscope/kimi-k2.5` or `moonshot/kimi-k2.5`, despite those exact prefixes being listed in Kimi compatibility tests. **Required fix shape:** (a) decide the supported routing prefixes for Kimi/DashScope/Moonshot and use one shared prefix-strip helper for compatibility checks and wire model serialization; (b) add `dashscope` and `moonshot` where appropriate, or reject those prefixed slugs early with a typed configuration error; (c) add tests proving `wire_model_for_base_url` and `strip_routing_prefix` produce `kimi-k2.5` for every documented Kimi prefix; (d) keep OpenRouter/non-default OpenAI base-url slash-slug preservation semantics intact; (e) update docs/config examples so model slugs and provider routing prefixes are unambiguous. **Why this matters:** provider compatibility logic and wire serialization must agree on the model identity. If one path says “this is Kimi” while another sends a prefixed slug the backend may not understand, users get avoidable 400/model-not-found errors that look like provider instability. Source: gaebal-gajae dogfood response to Clawhip message `1507307060998443059` on 2026-05-22.
575. **`grep_search` walks `.git`, `node_modules`, `target`, and other heavy directories even though `glob_search` skips them, so grep can waste startup/context time scanning generated/vendor files by default** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 10:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@819f67b`. Active tmux sessions at probe time: none. Code inspection: `rust/crates/runtime/src/file_ops.rs` defines `GLOB_SEARCH_IGNORED_DIRS` (`.git`, `node_modules`, `.build`, `target`, `dist`, `coverage`) and `glob_search_impl` applies it via `WalkDir::filter_entry(|entry| !should_skip_glob_dir(entry))`. But `grep_search_impl` calls `collect_search_files(&base_path)`, and `collect_search_files` uses raw `WalkDir::new(base_path)` with no `filter_entry` or ignore list. As a result, a default grep over a repo can descend through `.git/objects`, Rust `target/`, vendored `node_modules`, coverage, and dist outputs, then silently skip unreadable/non-UTF8 files (#571) or spend time decoding generated blobs before any model-visible answer. Existing tests prove `glob_search_skips_common_heavy_directories`, but no equivalent grep test exists. **Required fix shape:** (a) make `collect_search_files` share the same ignored-directory policy as `glob_search`, or define an explicit grep ignore policy with opt-in overrides; (b) add skipped/ignored directory counts to grep output so operators know scope was pruned; (c) add regressions where `.git`/`target` contain matching files but default grep ignores them, while direct path-to-file behavior remains explicit; (d) allow explicit include/override if users really want generated/vendor search; (e) align docs/tool schema so glob and grep default search scope semantics match. **Why this matters:** grep is a high-frequency dogfood/search tool. Scanning generated and VCS internals by default creates startup friction, noisy false matches, and token waste, especially in Rust/JS workspaces with huge `target` or `node_modules` trees. Source: gaebal-gajae dogfood response to Clawhip message `1507322157561151671` on 2026-05-22.
576. **`glob_search` brace expansion has no fan-out cap, so a single pattern with large brace groups can explode into thousands of `WalkDir` traversals before any timeout/result limit applies** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 10:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@ab5754a`. Active tmux sessions at probe time: none. Code inspection: `rust/crates/runtime/src/file_ops.rs::expand_braces` recursively expands the first `{...}` group by splitting on every comma and calling itself on each alternative. `glob_search_impl` then iterates every expanded pattern and creates a separate `WalkDir` traversal for each one before applying the hard `take(100)` result cap. There is no maximum number of alternatives, no recursion-depth/expanded-pattern cap, and no early deduplication by shared walk root. A user/model-supplied pattern like `**/*.{a,b,c,...hundreds}` or multiple brace groups can produce hundreds/thousands of full repo walks even though only 100 filenames can be returned. Existing tests cover a tiny `*.{rs,toml}` happy path and unmatched braces, but not expansion fan-out or timeout behavior. **Required fix shape:** (a) impose a small maximum expanded-pattern count and return `InvalidInput`/typed error when exceeded; (b) deduplicate shared walk roots or compile multiple patterns per root so alternatives do not trigger repeated full-tree walks; (c) include `expanded_pattern_count` and truncation/fanout metadata in diagnostics; (d) add regressions for large single brace groups, multiple nested brace groups, and normal small brace patterns; (e) keep the final 100-result cap but do not rely on it as protection against pre-result traversal explosions. **Why this matters:** glob is a model-facing discovery tool. Unbounded brace fan-out turns a compact pattern into many expensive filesystem walks, creating startup friction and an easy self-inflicted DoS before the existing result limit can help. Source: gaebal-gajae dogfood response to Clawhip message `1507329710257078353` on 2026-05-22.
577. **`WebFetch` upgrades only the initial HTTP URL, but follows redirects without revalidating the final target, so HTTPS pages can redirect the tool into localhost/private HTTP endpoints** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 11:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@571b4c2`. Active tmux sessions at probe time: none. Code inspection: `rust/crates/tools/src/lib.rs::normalize_fetch_url` upgrades an initial `http://` URL to HTTPS unless the host is exactly `localhost`, `127.0.0.1`, or `::1`. But `build_http_client` enables `redirect(reqwest::redirect::Policy::limited(10))`, and `execute_web_fetch` sends the request once, then trusts `response.url()` as the final URL. There is no redirect policy that re-runs scheme/host/IP checks for each hop, no block for redirects from public HTTPS to `http://localhost`, `http://127.0.0.1`, RFC1918/link-local/metadata IPs, or non-HTTPS final URLs. As a result, a model-requested public URL can legally redirect the tool into local/private network resources even though direct non-local HTTP is upgraded and no SSRF boundary is documented. **Required fix shape:** (a) implement a custom redirect policy that validates every `Location` target before following it; (b) reject redirects to localhost, loopback, link-local, RFC1918/private ranges, cloud metadata hosts/IPs, and/or downgrade-to-HTTP unless explicitly allowed; (c) resolve hostnames before fetch or after redirect to prevent DNS-based private IP hops; (d) add tests with public HTTPS -> localhost/private/metadata redirects and safe HTTPS->HTTPS redirects; (e) expose final URL plus redirect-block reason in the `WebFetch` output/error without leaking response bodies. **Why this matters:** WebFetch is an external content tool. Redirects are part of the fetch boundary, not a postscript; without per-hop validation, a harmless-looking public URL can become an internal network probe or local service read. Source: gaebal-gajae dogfood response to Clawhip message `1507337256107642961` on 2026-05-22.
578. **`WebSearch` accepts `CLAWD_WEB_SEARCH_BASE_URL` with any scheme/host and follows the shared redirect policy, so a local environment variable can turn search into a private-network fetch without guardrails** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 11:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@bff67c2`. Active tmux session at probe time: `gajae-issue-339-backlog-zero-candidate-selection`; no active claw-code implementation session. Code inspection: `rust/crates/tools/src/lib.rs::build_search_url` checks `CLAWD_WEB_SEARCH_BASE_URL`, parses it with `reqwest::Url::parse`, appends `q`, and returns it unchanged. Unlike `WebFetch`, there is no `normalize_fetch_url`-style scheme upgrade or host validation at all. `execute_web_search` then uses the same `build_http_client` as WebFetch, which follows up to 10 redirects. A poisoned or stale local env var can point search at `http://localhost`, RFC1918 services, metadata endpoints, or a redirector to those targets; the tool will fetch and parse generic links before domain allow/block filters are applied to extracted result URLs, not to the search endpoint itself. Existing tests/logic focus on hit filtering, not search-provider endpoint validation. **Required fix shape:** (a) validate `CLAWD_WEB_SEARCH_BASE_URL` at parse time against allowed schemes/hosts or require an explicit unsafe/local-test opt-in; (b) apply the same per-redirect target validation required by #577 to WebSearch; (c) distinguish configured test search providers from production search with a typed config/source field in output; (d) add regressions for env base URLs pointing to localhost/private/metadata and safe HTTPS test endpoints; (e) document the env var as test-only or constrain it to trusted public search domains. **Why this matters:** search is often treated as a low-risk public-web tool. An unvalidated base URL makes it an environment-controlled internal fetch surface, and result-domain filters do not protect the initial request or redirects. Source: gaebal-gajae dogfood response to Clawhip message `1507344805167108257` on 2026-05-22.
579. **Streaming flushes pending markdown at `ContentBlockStop` before pending tool calls are rendered, so text immediately followed by a tool call can be displayed before the tool-use event that actually interrupted/ended the content block** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 12:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@d8864ff`. Active tmux sessions at probe time: none. Channel context included Jobdori's #587 finding that `MarkdownStreamState::push` can hold prose until flush; this probe inspected the caller ordering. Code inspection: in `rust/crates/rusty-claude-cli/src/main.rs` stream handling, `ContentBlockDelta::TextDelta` buffers text through `markdown_stream.push`. On `ApiStreamEvent::ContentBlockStop`, the code first calls `markdown_stream.flush(&renderer)` and writes any pending prose, then only afterward checks `pending_tool.take()` and renders `format_tool_call_start`. If a provider emits a text block that has no safe boundary (single paragraph or unclosed markdown) followed by a tool-use block, the held text is flushed at the stop event just before the pending tool call display. Operators watching the terminal can see the assistant's prose burst immediately before the tool start marker, even though the next actionable event is the tool call; any timing/ordering cue that the model paused to call a tool is blurred by the delayed text flush. There is no test asserting streamed text/tool display ordering when markdown buffering holds content until block stop. **Required fix shape:** (a) make stream rendering preserve block/event order explicitly, perhaps flushing text at the exact text block stop and rendering tool-use starts at their own block starts/stops with clear separators; (b) when a text block is followed by a tool block, include a newline/phase boundary so delayed text does not visually merge into the tool call; (c) add streaming tests with single-paragraph text immediately followed by a tool call and with safe-boundary text, asserting terminal output order and separators; (d) consider the #587 word-boundary fallback so prose is not entirely held until the tool boundary; (e) keep persisted `AssistantEvent` ordering aligned with displayed output. **Why this matters:** streaming UI is also an event log. If buffered prose appears only when the next block stops and visually collides with a tool-use marker, users cannot tell whether the model is still speaking, has switched to tool execution, or the stream stalled. Source: gaebal-gajae dogfood response to Clawhip message `1507352360379482193` on 2026-05-22.
580. **Streaming safe-boundary detection treats any indented triple-backtick line as a fence opener but will not close it if the closing fence is indented more than three spaces, so quoted/list code blocks can freeze streaming until final flush** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 12:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@9f762b2`. Active tmux sessions at probe time: `gajae-pr-340-backlog-zero-candidate-selection-final-review`, `gajae-pr-340-backlog-zero-rereview2`; no active claw-code implementation session. Code inspection: `rust/crates/rusty-claude-cli/src/render.rs::parse_fence_opener` counts only literal spaces and accepts fence openers with indent <=3. Once inside a fence, `line_closes_fence` also requires the closing marker indent <=3. That matches CommonMark top-level fenced blocks, but streaming markdown from models often nests code fences under bullets, block quotes, or copied indentation where both opener and closer are indented four or more spaces. In that case the opener with >3 spaces is ignored (fine), but mixed/normalized output can still open at <=3 and then fail to close if the closer is rendered with extra list/quote indentation; `open_fence` remains set, `last_boundary` stops updating, and `MarkdownStreamState::push` returns `None` until `flush()`. Existing tests cover nested fence marker lengths and tilde/backtick distinction, but not list/blockquote/indented fence streaming behavior or a mismatched indentation close. **Required fix shape:** (a) decide whether the stream boundary detector should follow strict CommonMark or be tolerant for model-generated/list-nested fences; (b) if tolerant, close fences when the same marker appears after quote/list prefixes or consistent extra indentation, while still avoiding false closes inside literal code; (c) add tests for bullet-nested fences, blockquote fences, and an opener at indent <=3 with a closer at indent >3; (d) include a max-buffer/word-boundary fallback from #587 so a malformed fence cannot suppress all output indefinitely; (e) keep rendering tests aligned with boundary tests so visual output and streaming segmentation share one markdown dialect. **Why this matters:** model answers frequently include code inside bullets or quoted explanations. If the stream boundary tracker misses the closing fence, the terminal looks stalled even though deltas are arriving, turning a formatting edge case into startup/streaming opacity. Source: gaebal-gajae dogfood response to Clawhip message `1507359904959172758` on 2026-05-22.
581. **Terminal markdown renderer drops styling inside link labels because link text is accumulated as raw text while emphasis/inline-code events inside links are flattened before final rendering** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 13:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@8c4b33d`. Active tmux sessions at probe time: `gajae-issue-341-post-merge-cleanup-retention`, `omc-issue-3087-windows-hud-execfile`; no active claw-code implementation session. Code inspection: `rust/crates/rusty-claude-cli/src/render.rs::RenderState::append_raw` diverts all text into `link_stack.last_mut().text` whenever a link is open. `Event::Start(Tag::Emphasis)`, `Strong`, and inline `Code` still update global style/rendering, but when the current context is a link, the rendered text is appended to `LinkState.text` as plain accumulated label text. At `Event::End(TagEnd::Link)`, the renderer emits one uniformly underlined blue `[label](destination)` string. This means markdown like `[**important** docs](https://...)`, `[*emphasis* link](...)`, or ``[`code` API](...)`` loses bold/italic/inline-code styling inside the label, and ANSI escape sequences from nested inline code can be embedded into the label string before the final link styling in ways not covered by tests. Existing link coverage only asserts a simple `[Claw](url)` label. **Required fix shape:** (a) decide whether link labels should preserve nested inline styles or intentionally flatten them; (b) if preserving, store rendered segments or nested style spans in `LinkState` instead of one raw label string; (c) if flattening, strip ANSI/control styling from accumulated labels before final link render and document the limitation; (d) add tests for bold/emphasis/code inside links and nested links/images where the parser permits them; (e) ensure streaming chunk boundaries do not split ANSI state inside a link label. **Why this matters:** terminal rendering is the user-facing event log. Links often carry emphasized API names or inline-code identifiers; flattening or double-styling labels makes important documentation output less readable and can leak nested ANSI into one final styled link span. Source: gaebal-gajae dogfood response to Clawhip message `1507367459689201715` on 2026-05-22.
582. **Terminal table renderer ignores markdown alignment markers, so right/center-aligned numeric columns are rendered left-aligned with no test coverage** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 13:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@ccb319a`. Active tmux session at probe time: `omx-issue-2466-ultragoal-get-goal-recovery-resume`; no active claw-code implementation session. Code inspection: `rust/crates/rusty-claude-cli/src/render.rs` starts tables with `Event::Start(Tag::Table(..))` but discards the alignment vector carried by pulldown-cmark. `TableState` stores headers/rows/current cells only, and `render_table_row` always writes the cell text followed by padding spaces (`left` alignment) regardless of whether the markdown separator uses `:---`, `---:`, or `:---:`. Existing test `renders_tables_with_alignment` checks column width alignment only, using `| ---- | ----- |`, and does not cover right/center alignment semantics. **Required fix shape:** (a) store pulldown-cmark table alignment metadata in `TableState`; (b) apply left/center/right padding in `render_table_row` for headers and body cells; (c) add tests for `| left | center | right |` with `| :--- | :---: | ---: |`, including numeric columns; (d) ensure `visible_width` still handles ANSI-styled header cells correctly when padding is split before/after centered text; (e) document whether terminal rendering intentionally supports GitHub-style table alignment. **Why this matters:** tables are common in status reports, benchmarks, and cost/test summaries. Dropping right alignment makes numeric columns harder to scan and means the terminal renderer does not faithfully reflect markdown that users expect from GitHub/Discord-style output. Source: gaebal-gajae dogfood response to Clawhip message `1507375004671672391` on 2026-05-22.
583. **`PermissionEnforcer::check_file_write` uses string-prefix workspace checks, so relative traversal like `../../outside` is allowed as if it were inside the workspace** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 14:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@aa9efe5`. Active tmux session at probe time: `omx-issue-2468-autoresearch-docs-css`; no active claw-code implementation session. Channel context included Jobdori's #591 `Prompt`-mode bypass in `permission_enforcer.rs`, so this probe inspected the adjacent file-write boundary helper. Code inspection: `rust/crates/runtime/src/permission_enforcer.rs::is_within_workspace` treats relative paths by string-concatenating `format!("{workspace_root}/{path}")`, then checks `normalized.starts_with(root) || normalized == workspace_root.trim_end_matches('/')`. It never normalizes `.`/`..`, canonicalizes symlinks, or resolves the candidate path. Therefore `check_file_write("../../outside/secret.txt", "/workspace")` builds `/workspace/../../outside/secret.txt`, which still starts with `/workspace/`, and returns `Allowed` in `WorkspaceWrite` mode even though the resolved target is outside the workspace. Existing tests cover absolute outside paths and simple relative `src/main.rs`, but not relative traversal escapes. **Required fix shape:** (a) replace string-prefix checks with path normalization/canonicalization against a workspace root, resolving `.`/`..` before comparison; (b) reject escaping relative paths even if the file does not exist yet, using lexical normalization plus parent canonicalization where needed; (c) add regressions for `../outside.txt`, `../../outside/secret.txt`, `src/../safe.txt`, and symlink escape cases; (d) share the same boundary primitive with runtime file ops and bash path-scope checks so permission enforcement cannot drift; (e) include resolved/candidate path evidence in denial messages without leaking sensitive contents. **Why this matters:** workspace-write mode's core promise is “project files only.” A string-prefix check lets a caller smuggle outside writes through relative traversal while the permission enforcer reports success, defeating the boundary before lower-level file tools can help. Source: gaebal-gajae dogfood response to Clawhip message `1507382558340681779` on 2026-05-22.
584. **`WorkerRegistry::restart` clears prompt/trust state but leaves old events and the original `created_at`, so post-restart timeout evidence can mix prior-attempt blockers with the new boot attempt** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 14:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@ac09033`. Active tmux sessions at probe time: none. Channel context included Jobdori's worker-boot #592 finding, so this probe stayed in `worker_boot.rs` but checked restart lifecycle continuity. Code inspection: `rust/crates/runtime/src/worker_boot.rs::restart` resets `status`, trust flags, prompt fields, `last_error`, attempts, and `prompt_in_flight`, then appends `WorkerEventKind::Restarted`. It does not reset `worker.created_at`, does not create a per-attempt `boot_started_at`, does not increment a restart/attempt counter, and does not clear or partition previous events. Later `observe_startup_timeout` computes `elapsed = now.saturating_sub(worker.created_at)` and derives `trust_prompt_detected`, `tool_permission_detected`, and `ready_for_prompt_detected` by scanning the entire `worker.events` history. A worker that previously hit trust/tool/ready evidence, then restarts cleanly, can have the new timeout classified with old pre-restart evidence and an elapsed time anchored to the original creation. Existing `restart_and_terminate_reset_or_finish_worker` only asserts prompt fields/attempts reset; it does not assert event scoping or elapsed-time reset. **Required fix shape:** (a) add `current_attempt_started_at`/`boot_started_at` and `attempt_index` fields; (b) set them on create and restart and use them for startup timeout elapsed/command_started_at; (c) scope timeout evidence scans to events since the current attempt or store per-attempt cached evidence; (d) add tests where trust/tool evidence exists before restart but not after, proving the post-restart timeout does not inherit stale blockers; (e) include attempt index in worker events so dashboards can separate old and new boot attempts. **Why this matters:** restart is supposed to produce a fresh boot attempt. If old events and timestamps remain authoritative, operators see misleading “stalled for hours / trust required” evidence for a brand-new restart and recovery automation can choose the wrong next action. Source: gaebal-gajae dogfood response to Clawhip message `1507390103956226228` on 2026-05-22.
585. **Prompt-misdelivery auto-recovery arms replay without clearing `last_error`, so a worker can be `ReadyForPrompt` while still carrying a stale prompt-delivery failure** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 15:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@b0bca2e`. Active tmux session at probe time: `gajae-pr-346-session-gateway-continuity-digest-review`; no active claw-code implementation session. Code inspection: in `rust/crates/runtime/src/worker_boot.rs::observe`, prompt misdelivery sets `worker.last_error = Some(WorkerFailureKind::PromptDelivery)` and `prompt_in_flight = false`, then pushes a `PromptMisdelivery` event. If `worker.auto_recover_prompt_misdelivery` is true, it sets `worker.replay_prompt = worker.last_prompt.clone()` and `worker.status = WorkerStatus::ReadyForPrompt`, then pushes `PromptReplayArmed`; however it never clears or demotes `last_error`. `await_ready` then returns `ready: true` and `last_error: Some(PromptDelivery)`, so callers/dashboards can see a ready replay state and a failure state simultaneously. Later `send_prompt` clears `last_error`, but until replay is actually sent the state snapshot is contradictory. Existing replay tests assert `status == ReadyForPrompt` and `replay_prompt` contents, but do not assert `last_error` semantics while replay is armed. **Required fix shape:** (a) when auto-recovery arms replay, either clear `last_error` or replace it with a non-fatal/degraded `replay_armed` status separate from failure; (b) include `recovery_armed:true` in the worker snapshot or ready result so callers can distinguish a recoverable ready state from a failed state; (c) add tests asserting `await_ready` after auto-recovery does not report contradictory ready+fatal error; (d) preserve the original misdelivery event for audit history while keeping current worker state coherent; (e) ensure manual/non-auto recovery still reports failure until explicitly resolved. **Why this matters:** recovery state is an operator contract. A worker that is ready to replay should not also advertise a current fatal prompt-delivery error, or automation may both retry and escalate the same incident. Source: gaebal-gajae dogfood response to Clawhip message `1507397657763778562` on 2026-05-22.
586. **Plugin registry reads synchronously mutate bundled plugin installs without a lock/atomic swap, so startup/listing can race or fail while merely trying to aggregate hooks/tools** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 15:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@8043090`. Active tmux session at probe time: `gajae-pr-348-package-release-drift-review`; no active claw-code implementation session. Code inspection: every `PluginManager::plugin_registry_report` call begins with `self.sync_bundled_plugins()?`, and read-style paths (`plugin_registry`, `list_plugins`, `discover_plugins`, `aggregated_hooks`, `aggregated_tools`, startup `build_runtime_plugin_state_with_loader`) all flow through it. `sync_bundled_plugins` loads bundled manifests, then for each stale/outdated bundled plugin does `fs::remove_dir_all(&install_path)?; copy_dir_all(&source_root, &install_path)?;`, removes stale bundled IDs/directories, and finally writes `plugins/registry.json`. There is no process-wide file lock, no temp-dir + atomic rename, and no read-only/degraded mode. Two concurrent CLI startups or a startup plus `claw plugins list` can both decide a sync is needed; one can remove an install dir while the other is loading/copying it, yielding transient missing/partial plugin directories or a registry write race. Even a purely diagnostic/list/aggregate command can fail because bundled-plugin self-sync mutates disk before returning registry data. Existing tests cover sync happy paths and load-failure reporting, but not concurrent registry readers or a simulated remove/copy interruption. **Required fix shape:** (a) separate read-only registry discovery from bundled-plugin reconciliation, or gate reconciliation behind an explicit locked startup/update phase; (b) protect bundled install sync and registry writes with an interprocess lock; (c) copy bundled plugins into a temp dir and atomically rename/swap, never exposing partial installs; (d) if sync fails during a read-style command, return a degraded registry report with load failures instead of aborting all plugin aggregation where safe; (e) add concurrency/interruption tests with two managers racing `plugin_registry_report` and with `copy_dir_all` failure after removal, proving readers see either old or new complete plugin installs. **Why this matters:** plugin/MCP startup already has lifecycle friction. Registry reads should be safe and mostly observational; making them perform unlocked destructive replacement means diagnostics and startup can create the very plugin-load failures they are trying to observe. Source: gaebal-gajae dogfood response to Clawhip message `1507405207602987138` on 2026-05-22.
587. **`REPL` has an optional timeout with no default, so model-supplied code can block the tool dispatch thread indefinitely when `timeout_ms` is omitted** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 16:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@9843ccf`. Active tmux sessions at probe time: none. Channel context included Jobdori's adjacent #595 Sleep finding, so this probe inspected the other model-facing blocking execution surfaces instead of duplicating Sleep. Code inspection: `rust/crates/tools/src/lib.rs::execute_repl` validates code/language, spawns `python -c` / `node -e`, and only enters the polling timeout loop when `input.timeout_ms` is `Some`. If `timeout_ms` is omitted, it directly calls `process.spawn()?.wait_with_output()?`, with no default deadline, no backgrounding, and no abort-signal checks. The tool schema makes `timeout_ms` optional (`"timeout_ms": { "type": "integer", "minimum": 1 }`), and existing REPL success coverage passes `timeout_ms:500`; the timeout regression passes `timeout_ms:10`; there is no test for omitted-timeout long-running code. Therefore `REPL({language:"python", code:"import time; time.sleep(999999)"})` can freeze the same tool dispatch path indefinitely, which is worse than Sleep's 5-minute cap and easy for a model to trigger by forgetting the optional field. **Required fix shape:** (a) make `timeout_ms` default to a conservative deadline (for example 30s) instead of unbounded wait; (b) enforce an upper cap and return a structured timeout error matching bash/PowerShell timeout surfaces; (c) poll in small intervals that can observe a shared abort signal if/when the tool registry gains one; (d) add regressions for omitted timeout, explicit timeout, excessive timeout, and successful short code; (e) include elapsed/timeout metadata in the JSON error so claws can distinguish user-code hang from interpreter startup failure. **Why this matters:** REPL is a model-facing code execution tool. Optional timeout means the safest path depends on the model remembering to provide a guard every time; one missing field can wedge unattended claw runs forever with no heartbeat or typed recovery event. Source: gaebal-gajae dogfood response to Clawhip message `1507412753374249032` on 2026-05-22.
588. **Prompt-mode `read_piped_stdin()` still reads the entire pipe with no cap before merging it into the prompt, so the one-shot prompt path can OOM and API/session history can diverge from the persisted truncated JSONL** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 16:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@a1728b2`. Active tmux session at probe time: `omx-fooks-issue-1046-clean-epic-only-spawn-child`; no active claw-code implementation session. Channel context included Jobdori's #596 `LineEditor::read_line_fallback` unbounded line-read finding, so this probe checked the other piped-stdin entrypoint. Code inspection: `rust/crates/rusty-claude-cli/src/main.rs::read_piped_stdin` returns `None` for TTY stdin, then does `let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer)` with no byte/char cap. In `CliAction::Prompt`, when `permission_mode == DangerFullAccess`, this entire buffer is appended by `merge_prompt_with_stdin` to the user prompt and sent to `LiveCli::run_turn_with_output`. The session layer later truncates JSONL fields to `MAX_JSONL_FIELD_CHARS = 16 * 1024`, so a huge piped context can be fully allocated and sent to the provider while the persisted session records only a truncated copy. This is distinct from #596: `read_line_fallback` covers non-TTY REPL line input; `read_piped_stdin` covers explicit one-shot prompt + piped context. Existing tests exercise merge formatting but not large stdin caps or persistence parity. **Required fix shape:** (a) introduce one shared `MAX_STDIN_PROMPT_BYTES`/`MAX_STDIN_PROMPT_CHARS` boundary for both `read_line_fallback` and `read_piped_stdin`; (b) read through `take(limit + 1)` or chunked bounded reads so oversize input is detected before unbounded allocation; (c) fail with a typed “stdin prompt too large” error or summarize/truncate before both API send and session persistence using the same content; (d) add tests for empty stdin, normal small piped context, limit-boundary input, and oversize input; (e) include the stdin byte/char count and cap in JSON/text diagnostics without echoing the large payload. **Why this matters:** piped stdin is a primary automation path (`cat file | claw prompt ...`). If it reads unbounded context and then persists a different truncated transcript, claws cannot replay, audit, or recover the same conversation the provider actually saw. Source: gaebal-gajae dogfood response to Clawhip message `1507420302924185720` on 2026-05-22.
589. **Managed-proxy MCP transport has no auth field or `requires_user_auth` path, so protected proxy endpoints cannot use the same OAuth/auth lifecycle as HTTP/SSE** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 17:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@a93f36d`. Active tmux sessions at probe time: `gajae-issue-351-ops-surface-inventory-snapshot`, `omx-fooks-issue-1046-clean-epic-only-spawn-child`; no active claw-code implementation session. Channel context included Jobdori's #597 WebSocket OAuth gap, so this probe checked the remaining remote/proxy MCP transport shapes. Code inspection: `runtime/src/config.rs::McpManagedProxyServerConfig` has only `{ url, id }`, `parse_mcp_server_config` for `"claudeai-proxy"` parses only those two fields, `runtime/src/mcp_client.rs::McpClientTransport::ManagedProxy` stores `McpManagedProxyTransport { url, id }` with no `auth`, and `commands/src/lib.rs` reports only URL/proxy id in text/JSON. Unlike SSE/HTTP `McpRemoteTransport`, the managed-proxy path cannot carry `McpOAuthConfig`, cannot report `requires_user_auth()`, and cannot participate in the same user-auth/preflight lifecycle. A protected managed proxy must either be unauthenticated, rely on credentials encoded in URL/query (already a reporting leak class), or use an out-of-band mechanism invisible to `mcp list/show/doctor`. **Required fix shape:** (a) add `oauth: Option<McpOAuthConfig>` or an explicit managed-proxy auth config to `McpManagedProxyServerConfig`; (b) include `auth: McpClientAuth` in `McpManagedProxyTransport` or otherwise expose a shared `requires_user_auth` trait across all remote transports; (c) parse/report the auth requirement in `mcp show/list` without leaking tokens; (d) add tests for `claudeai-proxy` with OAuth proving bootstrap requires user auth and JSON/text surfaces expose non-secret auth metadata; (e) align with the WebSocket fix so every network MCP transport (SSE/HTTP/WS/managed-proxy) has symmetric auth configuration or an explicit documented reason why not. **Why this matters:** managed proxy is a remote MCP lifecycle path. If it cannot express auth, operators lose preflight visibility and are pushed toward URL/header secret hacks that diagnostics either leak (#90) or cannot validate. Source: gaebal-gajae dogfood response to Clawhip message `1507427853031968799` on 2026-05-22.
590. **Configured remote MCP transports are parsed and shown as first-class, but runtime `McpServerManager` only starts stdio servers and silently degrades every HTTP/SSE/WS/SDK/managed-proxy server into an unsupported registration failure** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 17:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@106c243`. Active tmux sessions at probe time: `gajae-issue-353-review-ci-verdict-transition-receipt`, `omx-fooks-issue-1046-clean-epic-only-spawn-child`; no active claw-code implementation session. Code inspection: `runtime/src/config.rs` parses transport variants `stdio`, `sse`, `http`, `ws`, `sdk`, and `claudeai-proxy`; `commands/src/lib.rs` renders their URL/header/OAuth/proxy details in `mcp list/show`. But `runtime/src/mcp_stdio.rs::McpServerManager::from_servers` inserts only `McpTransport::Stdio` into `managed_servers`; every other transport is pushed into `unsupported_servers` with reason `transport X is not supported by McpServerManager`. `discover_tools_best_effort` later turns those into `McpLifecyclePhase::ServerRegistration` failures/degraded startup, while `discover_tools` over `server_names()` ignores them entirely because `server_names()` only returns managed stdio server keys. Existing test `manager_records_unsupported_non_stdio_servers_without_panicking` locks the current behavior for http/sdk/ws as expected. **Required fix shape:** (a) decide whether remote transports are supported in this build; if not, make config parsing/show/list label them as `configured_but_runtime_unsupported` rather than implying they are usable; (b) if they are intended to work, implement separate managers/clients for HTTP/SSE/WS/managed-proxy and route discovery/tool calls by transport; (c) surface unsupported required remote servers as hard startup blockers in prompt/runtime preflight, and optional ones as typed degraded state, not just best-effort discovery noise; (d) add JSON fields in `mcp list/show/doctor` such as `runtime_supported:false`, `unsupported_reason`, and `required`; (e) add regressions proving `discover_tools` and prompt startup cannot silently ignore required non-stdio servers. **Why this matters:** users can configure and inspect remote MCP servers today, including auth fields for HTTP/SSE, but the actual runtime path does not run them. That split creates event/log opacity: `mcp show` looks configured while prompt-time tool discovery either degrades or omits the server, so operators cannot tell from control-plane surfaces whether remote MCP tools are actually reachable. Source: gaebal-gajae dogfood response to Clawhip message `1507435406277476393` on 2026-05-22.
591. **MCP degraded reports always compute `missing_tools` as empty because degraded construction passes `available_tools` (or `Vec::new()`) as the expected-tool set, so failed/unsupported servers never surface which tools disappeared** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 18:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@f50625a`. Active tmux sessions at probe time: `gajae-issue-355-backlog-zero-next-artifact-queue`, `omx-fooks-issue-1046-clean-epic-only-spawn-child`; no active claw-code implementation session. Code inspection: `runtime/src/mcp_lifecycle_hardened.rs::McpDegradedReport::new` can compute `missing_tools` by subtracting `available_tools` from `expected_tools`. But both producers discard the useful expected set: `runtime/src/mcp_stdio.rs::discover_tools_best_effort` passes `Vec::new()` for `expected_tools`, and `rusty-claude-cli/src/main.rs::RuntimeMcpState::new` passes `available_tools.clone()` as both `available_tools` and `expected_tools`. Therefore `missing_tools` is always empty, even when required servers fail discovery or unsupported remote transports are configured. Existing tests assert `degraded.missing_tools.is_empty()`, locking the broken contract. The only visible degraded data is failed server names/phases; operators cannot tell which qualified tool names vanished from the tool surface. **Required fix shape:** (a) retain each server's advertised/expected tool list from prior successful discovery, static config, manifest metadata, or at least the expected server/tool namespace when known; (b) pass that set into `McpDegradedReport::new` instead of `available_tools`/empty; (c) if exact tools are unknown, expose `missing_tool_names_unknown_for_servers:[...]` so the report is honest rather than falsely empty; (d) update tests to assert non-empty `missing_tools` or explicit unknown-missing metadata when a server with known tools fails; (e) thread the same degraded metadata into `ToolSearch` so models see which tools are unavailable, not just that a server failed. **Why this matters:** degraded startup is supposed to make partial success first-class. An always-empty `missing_tools` field falsely suggests no capability loss, hiding the actual impact of MCP failures and unsupported remote transports from both humans and automation. Source: gaebal-gajae dogfood response to Clawhip message `1507442954308948040` on 2026-05-22.
592. **Plugin degraded-mode reporting drops degraded-server errors because `PluginState::Degraded` only preserves failed server details, so a server marked degraded can appear fully healthy/available in the operator contract** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 18:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@1df4114`. Active tmux session at probe time: `gajae-issue-357-review-session-vanish-replacement`; no active claw-code implementation session. Focused validation: `cd rust && cargo test -p runtime plugin_lifecycle -- --nocapture` passed 6/6, confirming the current locked contract. Code inspection: `runtime/src/plugin_lifecycle.rs::PluginState::from_servers` treats `ServerStatus::Degraded` as usable by placing it in `healthy_servers`, but the `PluginState::Degraded` variant stores only `healthy_servers: Vec<String>` and `failed_servers: Vec<ServerHealth>`. `PluginHealthcheck::degraded_mode` then builds `unavailable_tools` only from `failed_servers` and reports a reason of `"N servers healthy, M servers failed"`. Existing test `degraded_server_status_keeps_server_usable` asserts a degraded `beta` server is included in `healthy_servers` and `failed_servers` is empty, but it does not assert that `beta`'s `last_error` (`high latency`) or degraded status is visible in `degraded_mode`. Result: a plugin with one healthy server and one degraded-but-usable server can emit `startup_degraded` while its degraded-mode payload says `2 servers healthy, 0 servers failed`, has no degraded server list, and can mark the degraded server's tools as simply available if discovery returns them. Operators and automation see the terminal state but lose the actual degraded reason/capability risk. **Required fix shape:** (a) split `PluginState::Degraded` into `healthy_servers`, `degraded_servers: Vec<ServerHealth>`, and `failed_servers`, or preserve all non-healthy server health records; (b) make `degraded_mode` include `degraded_tools`/`degraded_servers` with `last_error` separately from unavailable failed tools; (c) update the reason string/counts to distinguish healthy, degraded, and failed rather than folding degraded into healthy; (d) add tests proving a `ServerStatus::Degraded` server's status/error/capabilities appear in the degraded-mode JSON while remaining callable when appropriate; (e) align this with MCP degraded reports so partial plugin startup carries both availability and quality-of-service impact. **Why this matters:** partial startup needs impact opacity removed. A degraded server is not failed, but it also is not fully healthy; folding it into the healthy list makes `startup_degraded` hard to diagnose and can trick recovery logic into thinking no server has actionable degradation details. Source: gaebal-gajae dogfood response to Clawhip message `1507450501925441616` on 2026-05-22.
593. **Plugin uninstall is a three-store non-atomic transaction: it deletes the plugin directory before persisting registry/settings cleanup, so an IO failure can leave registry and `enabledPlugins` pointing at a removed plugin** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 19:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@244bdb7`. Active tmux session at probe time: `gajae-pr-358-review-session-vanish-replacement-review`; no active claw-code implementation session. Focused validation: `cd rust && cargo test -p plugins installs_enables_updates_and_uninstalls_external_plugins -- --nocapture` passed 1/1, confirming the happy-path lifecycle test does not cover interrupted persistence. Code inspection: `plugins/src/lib.rs::PluginManager::uninstall` removes the plugin record from the in-memory registry, then `remove_dir_all(record.install_path)`, then `store_registry(&registry)`, then `write_enabled_state(plugin_id, None)`. If directory removal succeeds but `store_registry` fails, `installed.json` still records the old plugin while the on-disk install directory is gone. If `store_registry` succeeds but `write_enabled_state` fails, the registry and disk are removed but `.claw/settings.json` can still contain `enabledPlugins[plugin_id]=true`. The next `plugin_registry_report` may silently prune the stale registry entry, but the enabled state can remain as orphaned configuration noise and the original uninstall command has already destroyed the install before it can report a clean transactional result. Existing `installs_enables_updates_and_uninstalls_external_plugins` asserts only the success path; it does not simulate registry write failure, settings write failure, or crash after `remove_dir_all`. **Required fix shape:** (a) make plugin uninstall a transaction with a tombstone/backup phase: first persist intended disabled/removed state or acquire a lock, then move the install directory to a same-filesystem trash/backup path, then update registry/settings, then remove backup after all metadata commits succeed; (b) if metadata cleanup fails, restore or report a recoverable tombstoned state with `rollback_available:true`; (c) make stale enabled settings for missing plugins produce a structured warning and auto-clean only through a safe metadata path; (d) add tests injecting failures after directory move/removal, after `store_registry`, and after `write_enabled_state`; (e) reuse the atomic JSON/write-lock helper from #508 so registry/settings writes cannot be partially written. **Why this matters:** uninstall is the destructive plugin lifecycle verb. Deleting files before committing metadata means a transient settings/registry IO failure turns a local cleanup into stale-branch-style plugin confusion: inventory can say installed/enabled while the executable hook/tool files are already gone. Source: gaebal-gajae dogfood response to Clawhip message `1507458055812546630` on 2026-05-22.
594. **MCP stdio frame reader trusts arbitrary `Content-Length` and allocates that many bytes before reading, so a buggy or hostile MCP server can OOM the runtime with one header** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 19:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@d996b65`. Active tmux session at probe time: `gajae-pr-358-review-session-vanish-replacement-review`; no active claw-code implementation session. Focused validation: `cd rust && cargo test -p runtime mcp_stdio -- --nocapture` passed 22/22, confirming current MCP stdio tests cover normal/mismatched/lowercase frames but not oversized frames. Code inspection: `runtime/src/mcp_stdio.rs::McpStdioProcess::read_frame` parses `Content-Length` into `usize`, then immediately does `let mut payload = vec![0_u8; content_length]; self.stdout.read_exact(&mut payload).await?;`. There is no maximum frame size, no per-server byte budget, no early rejection on huge lengths, and no streaming cap. A server can send `Content-Length: 10000000000
` (or any value near available memory) and force allocation before any payload bytes arrive; the surrounding `run_process_request` timeout does not protect the allocation itself. This is distinct from HTTP body caps (#503) and SSE parser buffering (#506): it is the MCP JSON-RPC stdio framing layer. Existing tests assert lowercase `Content-Length`, missing/mismatched IDs, timeout, and retry/reset behavior, but none assert a maximum accepted frame length. **Required fix shape:** (a) add a conservative `MAX_MCP_STDIO_FRAME_BYTES` default and optional per-server override; (b) after parsing `Content-Length`, reject values above the cap with `io::ErrorKind::InvalidData` carrying `content_length` and `max_frame_bytes`; (c) read the body through a bounded buffer/helper so allocation is capped and timeout/error surfaces stay typed as MCP invalid response; (d) add regression scripts that emit huge `Content-Length` with no body and oversized body, proving no large allocation and a structured invalid-response error; (e) include frame-size metadata in MCP degraded/error reports so operators can distinguish protocol abuse from transport EOF. **Why this matters:** MCP servers are extension processes. The client must treat their stdio as untrusted protocol input; one oversized length header should not be able to OOM a prompt startup, tool discovery, or resource read before degraded-mode reporting can fire. Source: gaebal-gajae dogfood response to Clawhip message `1507465601499660349` on 2026-05-22.
595. **OAuth authorize URL builder allows `extra_params` to override core PKCE/OAuth parameters after they were already set, so plugin/config extras can replace `state`, `code_challenge`, `redirect_uri`, or `response_type`** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 20:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@46f3bff`. Active tmux sessions at probe time: none; no active claw-code implementation session. Focused validation: `cd rust && cargo test -p runtime oauth -- --nocapture` passed 9/9, confirming current OAuth tests cover happy-path URL/form/callback parsing but not reserved extra-param collisions. Code inspection: `runtime/src/oauth.rs::OAuthAuthorizationRequest::build_url` creates a `params` vector containing core parameters (`response_type=code`, `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `code_challenge_method`), then blindly `extend`s `self.extra_params` into the same query. `with_extra_param` accepts any key and stores it in a `BTreeMap`, with no reserved-name validation. A caller that sets `with_extra_param("state", "attacker")`, `code_challenge`, `redirect_uri`, `response_type`, `client_id`, or `scope` produces a URL with duplicate query parameters where the extra value appears after the core value. Because many OAuth parsers use last-value-wins semantics, this can desynchronize the locally expected state/PKCE verifier from the authorization-server-visible values, or change redirect/scope semantics. Jobdori separately filed duplicate callback parameters (#603); this is the outbound sibling: duplicates are generated by the client itself before the browser redirect, not just accepted on callback. **Required fix shape:** (a) define a reserved parameter set for OAuth authorization requests (`response_type`, `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `code_challenge_method`) and reject attempts to add them via `with_extra_param`; (b) make `with_extra_param` return `Result<Self, OAuthError>` or validate in `build_url` with a typed error rather than silently emitting duplicates; (c) add tests for reserved collisions (`state`, `code_challenge`, `redirect_uri`) and a safe extension like `login_hint`; (d) if an override is intentionally supported, make it explicit and update the stored expected state/verifier/redirect to match so callback/token exchange cannot drift; (e) document provider-specific extra params as additive-only. **Why this matters:** `state` and PKCE are the OAuth anti-CSRF/proof-of-possession controls. Letting arbitrary extras duplicate or override them in the authorization URL creates prompt/auth lifecycle ambiguity and can turn a provider-specific hint hook into a security-sensitive parameter injection footgun. Source: gaebal-gajae dogfood response to Clawhip message `1507473155273265172` on 2026-05-22.
596. **MCP tool bridge gates `call_tool` on a stale in-memory registry snapshot before doing live discovery, so newly discovered tools can be rejected and removed tools can be offered until runtime failure** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 20:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@20c9d9d`. Active tmux sessions at probe time: none; no active claw-code implementation session. Focused validation: `cd rust && cargo test -p runtime mcp_tool_bridge -- --nocapture` passed 19/19, confirming current bridge tests cover pre-registered happy-path tools but not registry/manager drift. Code inspection: `runtime/src/mcp_tool_bridge.rs::McpToolRegistry::call_tool` first locks `self.inner`, requires `state.status == Connected`, and checks `state.tools.iter().any(|t| t.name == tool_name)`. Only after that snapshot gate does it drop the registry lock and call `spawn_tool_call`, which creates a runtime and runs `manager.discover_tools().await` followed by `manager.call_tool(...)`. The live discovery result updates the manager/tool index, but the registry snapshot used for admission is never refreshed from that discovery. Therefore a tool that becomes available after startup/discovery refresh is still rejected as `tool not found` if it is absent from the stale registry, while a tool that disappeared can remain listed/accepted by the registry and then fail later as an `UnknownTool`/runtime error from the manager. Existing tests explicitly register `echo` in the registry before calling the live manager, so they lock in the assumption that the registry already matches runtime discovery. **Required fix shape:** (a) make `call_tool` reconcile registry state from `manager.discover_tools()` before the tool-existence gate, or delegate existence checks entirely to the authoritative manager and then update the registry snapshot; (b) add a registry `last_discovered_at`/generation or per-server tool-source metadata so `list_tools` can report stale/degraded state; (c) add regressions where the registry starts empty but the manager discovers `echo`, and where the registry advertises a stale tool that the manager no longer exposes; (d) ensure `list_tools`, `ToolSearch`, and `call_tool` agree on the same generation/tool surface; (e) surface drift as a typed MCP lifecycle/degraded event rather than a generic `tool not found`. **Why this matters:** the bridge is a control-plane contract between model-visible MCP tools and the live server manager. If admission checks use an old snapshot while execution uses fresh discovery, claws get stale tool availability evidence and confusing failures exactly where autonomous recovery needs a single source of truth. Source: gaebal-gajae dogfood response to Clawhip message `1507480700868362384` on 2026-05-22.
597. **Worker prompt replay accepts a new `task_receipt` even when replaying a recovered prompt, so auto-recovery can pair the old prompt text with a new/different execution receipt** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 21:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@71f8554`. Active tmux sessions at probe time: none; no active claw-code implementation session. Focused validation: `cd rust && cargo test -p runtime worker_boot -- --nocapture` passed 22/22, confirming current worker-boot tests cover replay happy path and receipt mismatch detection after observation but not replay receipt integrity. Code inspection: `runtime/src/worker_boot.rs::WorkerRegistry::send_prompt` computes `next_prompt` from the explicit `prompt` argument or falls back to `worker.replay_prompt.clone()`, then unconditionally sets `worker.expected_receipt = task_receipt`. The tool input (`tools/src/lib.rs::WorkerSendPromptInput`) allows `prompt: Option<String>` and `task_receipt: Option<WorkerTaskReceipt>` independently. After prompt misdelivery, `observe` sets `worker.replay_prompt = worker.last_prompt.clone()` and payloads include the original `worker.expected_receipt`. But a subsequent `send_prompt(worker_id, None, Some(new_receipt))` replays the old recovered prompt while replacing the expected receipt with a new one. Conversely, replaying with `None` drops the expected receipt entirely. The later wrong-task-receipt detector compares observed receipts against this newly supplied/dropped value, not the receipt that belonged to the misdelivered prompt. Existing tests `prompt_misdelivery_is_detected_and_replay_can_be_rearmed` and `wrong_task_receipt_mismatch_is_detected_before_execution_continues` do not assert that replay preserves the original receipt or rejects receipt changes. **Required fix shape:** (a) when `prompt` is omitted and `worker.replay_prompt` is used, preserve the original `worker.expected_receipt` and reject any conflicting `task_receipt`; (b) store replay as a structured `{prompt, task_receipt}` bundle instead of only `String`; (c) if callers intentionally want a new prompt/receipt, require an explicit non-empty `prompt` and clear the replay bundle with an event; (d) add regressions for replay-with-new-receipt rejection, replay-with-no-receipt preserving the original receipt, and explicit new prompt replacing both prompt and receipt; (e) include receipt id/hash in `PromptReplayArmed`/`Running` event payloads without leaking sensitive prompt text. **Why this matters:** prompt recovery is supposed to resend the same task that was misdelivered. Letting the control plane silently combine old prompt text with a new or missing receipt breaks the provenance chain, causing stale/incorrect task execution evidence and making misdelivery recovery untrustworthy. Source: gaebal-gajae dogfood response to Clawhip message `1507488254734106685` on 2026-05-22.
598. **Sub-agent lane manifests emit `LaneEvent::started/blocked/failed/finished/commit_created` with minimal optional metadata, so exported lane events fail the stricter G004 `emitterIdentity`/`environmentLabel` contract and have duplicate `seq=0` ordering** — dogfooded 2026-05-22 from the `#clawcode-building-in-public` 21:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@d2018d7`. Active tmux sessions at probe time: none; no active claw-code implementation session. Channel context included Jobdori filing #606 for the general G004 validator/model mismatch; this pinpoint is the concrete producer-side lane-manifest impact. Focused validation: `cd rust && cargo test -p tools lane -- --nocapture` passed 8/8, confirming current tools lane tests cover canonical event names/failure taxonomy but not G004 conformance of generated sub-agent manifests. Code inspection: `tools/src/lib.rs::create_agent_with_spawn` initializes `AgentOutput.lane_events` with `LaneEvent::started(iso8601_now())`; `persist_agent_terminal_state` appends `LaneEvent::blocked`, `LaneEvent::failed`, `LaneEvent::finished(...).with_data(...)`, and `LaneEvent::commit_created(...)`. All of these constructors call `LaneEvent::new`, which uses `LaneEventMetadata::new(0, EventProvenance::LiveLane)` and leaves `environment_label`/`emitter_identity` as `None`. Because metadata fields are skipped when `None`, serialized manifests omit the exact fields that `g004_conformance.rs::validate_lane_events` requires, and every appended event also carries `metadata.seq = 0`, violating the validator's strictly-increasing sequence rule as soon as a manifest has multiple events. This means even if the data model is fixed broadly, current sub-agent manifest production still needs a generation path that fills emitter/environment and monotonic seq. **Required fix shape:** (a) route all sub-agent lane event creation through `LaneEventBuilder` or a manifest-local helper that assigns monotonic sequence numbers; (b) set non-secret defaults such as `emitterIdentity:"tools.subagent"` and `environmentLabel` from cwd/session/channel/config, never leave them absent for exported manifests; (c) add a test that creates and completes/fails a sub-agent manifest then validates its `lane_events` with `validate_g004_contract_bundle` or a lane-event-specific conformance helper; (d) update helper constructors or document them as internal/non-G004 only; (e) align with #606 so constructor defaults and validator requirements agree. **Why this matters:** lane manifests are the event-log surface for autonomous sub-agents. If their generated events cannot pass the repo's own conformance validator and all share seq 0, downstream dashboards/reconcilers cannot rely on ordering, ownership, or environment provenance. Source: gaebal-gajae dogfood response to Clawhip message `1507495807052550174` on 2026-05-22.