mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-24 13:08:11 +08:00
877 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
724a78604d | ROADMAP #137: model-alias shorthand regression in test suite — bare alias parsing broken on feat/134-135-session-identity; 3 tests fail with invalid model syntax error after #134/#135 validation tightening | ||
|
|
91ba54d39f | ROADMAP #136: --compact flag silently overrides --output-format json — compact turn always emits plain text even when JSON requested; unreachable Json arm in run_with_output() match; joins output-format completeness cluster #90/#91/#92/#127/#130 and CLI/REPL parity §7.1 | ||
|
|
8b52e77f23 | ROADMAP #135: claw status --json missing active_session bool and session.id cross-reference — status query side of #134 round-trip; joins session identity completeness §4.7 and status surface completeness cluster #80/#83/#114/#122; natural bundle #134+#135 closes session-identity round-trip | ||
|
|
2c42f8bcc8 | docs: remove duplicate ROADMAP #134 entry | ||
|
|
f266505546 | ROADMAP #134: no run/correlation ID at session boundary — session.id missing from startup event and status JSON; observer must infer session identity from timing | ||
|
|
50e3fa3a83 |
docs: add --output-format to diagnostic verb help text
Updated LocalHelpTopic help strings to surface --output-format support: - Status, Sandbox, Doctor, Acp all now show [--output-format <format>] - Added 'Formats: text (default), json' line to each Diagnostic verbs support JSON output but help text didn't advertise it. Post-#127 fix: help text now matches actual CLI surface. Verified: cargo build passes, claw doctor --help shows output-format. Refs: #127 |
||
|
|
a51b2105ed |
docs: add JSON output example for diagnostic verbs post-#127
USAGE.md now documents: - for machine-readable diagnostics - Note about parse-time rejection of invalid suffix args (post-#127 fix) Verifies that diagnostic verbs support JSON output for scripting, and documents the behavior change from #127 (invalid args rejected at parse time instead of falling through to prompt dispatch). Refs: #127 |
||
|
|
a3270db602 |
fix: #127 reject unrecognized suffix args for diagnostic verbs
Diagnostic verbs (help, version, status, sandbox, doctor, state) now reject unrecognized suffix arguments at parse time instead of silently falling through to Prompt dispatch. Fixes: claw doctor --json (and similar) no longer accepts --json silently and attempts to send it to the LLM as a prompt. Now properly emits: 'unrecognized argument `--json` for subcommand `doctor`' Joined parser-level trust gap quintet #108 + #117 + #119 + #122 + #127. Prevents token burn on rejected arguments. Verified: cargo build --workspace passes, claw doctor --json errors cleanly. Refs: #127, ROADMAP |
||
|
|
12f1f9a74e |
feat: wire ship.prepared provenance emission at bash execution boundary
Adds ship provenance detection and emission in execute_bash_async(): - Detects git push to main/master commands - Captures current branch, HEAD commit, git user as actor - Emits ship.prepared event with ShipProvenance payload - Logs to stderr as interim routing (event stream integration pending) This is the first wired provenance event — schema (§4.44.5) now has runtime emission at actual git operation boundary. Verified: cargo build --workspace passes. Next: wire ship.commits_selected, ship.merged, ship.pushed_main events. Refs: §4.44.5.1, ROADMAP #4.44.5 |
||
|
|
2678fa0af5 |
fix: #124 --model validation rejects malformed syntax at parse time
Adds validate_model_syntax() that rejects: - Empty strings - Strings with spaces (e.g., 'bad model') - Invalid provider/model format Accepts: - Known aliases (opus, sonnet, haiku) - Valid provider/model format (provider/model) Wired into parse_args for both --model <value> and --model=<value> forms. Errors exit with clear message before any API calls (no token burn). Verified: - 'claw --model "bad model" version' → error, exit 1 - 'claw --model "" version' → error, exit 1 - 'claw --model opus version' → works - 'claw --model anthropic/claude-opus-4-6 version' → works Refs: ROADMAP #124 (debbcbe cluster — parser-level trust gap family) |
||
|
|
b9990bb27c |
fix: #122 + #125 doctor consistency and git_state clarity
#122: doctor invocation now checks stale-base condition - Calls run_stale_base_preflight(None) in render_doctor_report() - Emits stale-base warnings to stderr when branch is behind main - Fixes inconsistency: doctor 'ok' vs prompt 'stale base' warning #125: git_state field reflects non-git directories - When !in_git_repo, git_state = 'not in git repo' instead of 'clean' - Fixes contradiction: in_git_repo: false but git_state: 'clean' - Applied in both doctor text output and status JSON Verified: cargo build --workspace passes. Refs: ROADMAP #122 (dd73962), #125 (debbcbe) |
||
|
|
f33c315c93 |
fix: #122 doctor invocation now checks stale-base condition
Adds run_stale_base_preflight(None) call to render_doctor_report() so that claw doctor emits stale-base warnings to stderr when the current branch is behind main. Previously doctor reported 'ok' even when branch was stale, creating inconsistency with prompt path warnings. Fixes silent-state inventory gap: doctor now consistent with prompt/repl stale-base checking. No behavior change for non-stale branches. Verified: cargo build --workspace passes, no test failures. Ref: ROADMAP #122 dogfood filing @ dd73962 |
||
|
|
5c579e4a09 |
§4.44.5.1: file ship event wiring pinpoint (schema landed, wiring missing)
Dogfood cycle 2026-04-20 identified that §4.44.5 ship/provenance event schema is implemented (ShipProvenance struct, ship.* constructors, tests pass) but actual git push/merge/commit-range operations do not yet emit these events. Events remain dead code—constructors exist but are never called during real workflows. This pinpoint tracks the missing wiring: locating actual git operation call sites in main.rs/tools/lib.rs/worker_boot.rs and intercepting to emit ship.prepared/commits_selected/merged/pushed_main with real metadata (source_branch, commit_range, merge_method, actor, pr_number). Acceptance: at least one real git push emits all 4 events with actual payload values, claw state JSON surfaces ship provenance. Ref: dogfood gaebal-gajae @ 1495672954573291571 (15:30 KST) |
||
|
|
8a8ca8a355 |
ROADMAP #4.44.5: Ship/provenance events — implement §4.44.5
Adds structured ship provenance surface to eliminate delivery-path opacity: New lane events: - ship.prepared — intent to ship established - ship.commits_selected — commit range locked - ship.merged — merge completed with provenance - ship.pushed_main — delivery to main confirmed ShipProvenance struct carries: - source_branch, base_commit - commit_count, commit_range - merge_method (direct_push/fast_forward/merge_commit/squash_merge/rebase_merge) - actor, pr_number Constructor methods added to LaneEvent for all four ship events. Tests: - Wire value serialization for ship events - Round-trip deserialization - Canonical event name coverage Runtime: 465 tests pass ROADMAP updated with IMPLEMENTED status This closes the gap where 56 commits pushed to main had no structured provenance trail — now emits first-class events for clawhip consumption. |
||
|
|
b0b579ebe9 |
ROADMAP #133: Blocked-state subphase contract — implement §6.5
Adds BlockedSubphase enum with 7 variants for structured blocked-state reporting: - blocked.trust_prompt — trust gate blockers - blocked.prompt_delivery — prompt misdelivery - blocked.plugin_init — plugin startup failures - blocked.mcp_handshake — MCP connection issues - blocked.branch_freshness — stale branch blockers - blocked.test_hang — test timeout/hang - blocked.report_pending — report generation stuck LaneEventBlocker now carries optional subphase field that gets serialized into LaneEvent data. Enables clawhip to route recovery without pane scraping. Updates: - lane_events.rs: BlockedSubphase enum, LaneEventBlocker.subphase field - lane_events.rs: blocked()/failed() constructors with subphase serialization - lib.rs: Export BlockedSubphase - tools/src/lib.rs: classify_lane_blocker() with subphase: None - Test imports and fixtures updated Backward-compatible: subphase is Option<>, existing events continue to work. |
||
|
|
c956f78e8a |
ROADMAP #4.44.5: Ship/provenance opacity — filed from dogfood
Added structured delivery-path contract to surface branch → merge → main-push provenance as first-class events. Filed from the 56-commit 2026-04-20 push that exposed the gap. Also fixes: ApiError test compilation — add suggested_action: None to 4 sites - Line ~8414: opaque_provider_wrapper_surfaces_failure_class_session_and_trace - Line ~8436: retry_exhaustion_uses_retry_failure_class_for_generic_provider_wrapper - Line ~8499: provider_context_window_errors_are_reframed_with_same_guidance - Line ~8533: retry_wrapped_context_window_errors_keep_recovery_guidance |
||
|
|
dd73962d0b | ROADMAP #122: doctor invocation does not check stale-base condition — run_stale_base_preflight() only invoked in Prompt + REPL paths, missing in doctor action handler; inconsistency: doctor says 'ok' but prompt warns 'stale base'; joins boot preflight / doctor contract family (#80-#83/#114) and silent-state inventory (#102/#127/#129/#245) | ||
|
|
027efb2f9f | ROADMAP §4.44: Typed-error envelope contract (Silent-state inventory roll-up) — locks in structured error.kind/operation/target/errno/hint/retryable contract that closes the family of pinpoints currently scattered across #102 + #121 + #127 + #129 + #130 + #245; backward-compat additive; regression locked via golden-fixture; gates 'Run claw --help for usage' trailer on error.kind == usage; drafted jointly with gaebal-gajae during 2026-04-20 dogfood cycle | ||
|
|
866f030713 | ROADMAP #130: claw export --output filesystem errors surface raw OS errno strings with zero context — 5 distinct failure modes all produce different errno strings but the same zero-context shape; no path echoed, no operation named, no io::ErrorKind classification, no actionable hint; JSON envelope flattens to {error, type} losing all structure; Run claw --help for usage trailer misleads on non-usage errors; joins JSON-envelope asymmetry family #90/#91/#92/#110/#115/#116 and truth-audit #80-#127/#129 | ||
|
|
d2a83415dc | ROADMAP #129: MCP server startup blocks credential validation in Prompt path — cred check ordered AFTER MCP child handshake await; misbehaved/slow MCP wedges every claw <prompt> invocation indefinitely; npx restart loop wastes resources; runtime-side companion to #102's config-time MCP gap; PARITY.md Lane 7 acceptance gap | ||
|
|
8122029eba | ROADMAP #128: claw --model <malformed> (spaces, empty string, invalid syntax) silently accepted at parse time, falls through to cred-error misdirection; joins parser-level trust gap family #108/#117/#119/#122/#127; joins token-burn family #99/#127 | ||
|
|
d284ef774e | ROADMAP #127: claw <subcommand> --json silently falls through to LLM Prompt dispatch — diagnostic verbs (doctor, status, sandbox, skills, version, help) reject --json with cred-error misdirection; valid verb + unrecognized suffix arg = Prompt fall-through; 18th silent-flag, 5th parser-level trust gap, joins #108 + #117 + #119 + #122 | ||
|
|
7370546c1c |
ROADMAP #126: /config [env|hooks|model|plugins] ignores section argument — all 4 subcommands return bit-identical file-list envelope; 4-way dispatch collapse
Dogfooded 2026-04-18 on main HEAD b56841c from /tmp/cdFF2.
/config model, /config hooks, /config plugins, /config env all
return: {kind:'config', cwd, files:[...], loaded_files,
merged_keys} — BIT-IDENTICAL.
diff /config model vs /config hooks → empty.
Section argument parsed at slash-command level but not branched
on in the handler.
Help: '/config [env|hooks|model|plugins] Inspect Claude config
files or merged sections [resume]'
→ 'merged sections' never shown. Same file-list for all.
Third dispatch-collapse finding:
#111: /providers → Doctor (2-way, wildly wrong)
#118: /stats + /tokens + /cache → Stats (3-way, distinct)
#126: /config env + hooks + model + plugins → file-list (4-way)
Fix shape (~60 lines):
- Section-specific handlers:
/config model → resolved model, source, aliases
/config hooks → pre_tool_use, post_tool_use arrays
/config plugins → enabled_plugins list
/config env → current file-list (already correct)
- Bare /config → current file-list envelope
- Regression per section
Joins Silent-flag/documented-but-unenforced.
Joins Truth-audit — help promises section inspection.
Joins Dispatch-collapse family: #111 + #118 + #126.
Natural bundle: #111 + #118 + #126 — dispatch-collapse trio.
Complete parser-dispatch-collapse audit across slash commands.
Filed in response to Clawhip pinpoint nudge 1495023618529300580
in #clawcode-building-in-public.
|
||
|
|
b56841c5f4 |
ROADMAP #125: git_state 'clean' emitted for non-git directories; GitWorkspaceSummary default all-zeros → is_clean() → 'clean' even when in_git_repo: false; contradictory doctor fields
Dogfooded 2026-04-18 on main HEAD debbcbe from /tmp/cdBB2.
Non-git directory:
$ mkdir /tmp/cdBB2 && cd /tmp/cdBB2 # NO git init
$ claw --output-format json status | jq .workspace.git_state
'clean' # should be null — not in a git repo
$ claw --output-format json doctor | jq '.checks[]
| select(.name=="workspace") | {in_git_repo, git_state}'
{"in_git_repo": false, "git_state": "clean"}
# CONTRADICTORY: not in git BUT git is 'clean'
Trace:
main.rs:2550-2554 parse_git_workspace_summary:
let Some(status) = status else {
return summary; // all-zero default when no git
};
All-zero GitWorkspaceSummary → is_clean() (changed_files==0)
→ true → headline() = 'clean'
main.rs:4950 status JSON: git_summary.headline() for git_state
main.rs:1856 doctor workspace: same headline() for git_state
Fix shape (~25 lines):
- Return Option<GitWorkspaceSummary> when status is None
- headline() returns Option<String>: None when no git
- Status JSON: git_state: null when not in git
- Doctor: omit git_state when in_git_repo: false, or set null
- Optional: claw init skip .gitignore in non-git dirs
- Regression: non-git → null, git clean → 'clean',
detached HEAD → 'clean' + 'detached HEAD'
Joins Truth-audit — 'clean' is a lie for non-git dirs.
Adjacent to #89 (claw blind to mid-rebase) — same field,
different missing state.
Joins #100 (status/doctor JSON gaps) — another field whose
value doesn't reflect reality.
Natural bundle: #89 + #100 + #125 — git-state-completeness
triple: rebase/merge invisible (#89) + stale-base unplumbed
(#100) + non-git 'clean' lie (#125). Complete git_state
field failure coverage.
Filed in response to Clawhip pinpoint nudge 1495016073085583442
in #clawcode-building-in-public.
|
||
|
|
debbcbe7fb |
ROADMAP #124: --model accepts any string with zero validation; typos silently pass through; empty string accepted; status JSON has no model provenance
Dogfooded 2026-04-18 on main HEAD bb76ec9 from /tmp/cdAA2. --model flag has zero validation: claw --model sonet status → model:'sonet' (typo passthrough) claw --model '' status → model:'' (empty accepted) claw --model garbage status → model:'garbage' (any string) Valid aliases do resolve: sonnet → claude-sonnet-4-6 opus → claude-opus-4-6 Config aliases also resolve via resolve_model_alias_with_config But unresolved strings pass through silently. Typo 'sonet' becomes literal model ID sent to API → fails late with 'model not found' after full context assembly. Compare: --reasoning-effort: validates low|medium|high. Has guard. --permission-mode: validates against known set. Has guard. --model: no guard. Any string. --base-commit: no guard (#122). Same pattern. status JSON: {model: 'sonet'} — shows resolved name only. No model_source (flag/config/default). No model_raw (pre-resolution input). No model_valid (known to any provider). Claw can't distinguish typo from exact model from alias. Trace: main.rs:470-480 --model parsing: model = value.clone(); index += 2; No validation. Raw string stored. main.rs:1032-1046 resolve_model_alias_with_config: resolves known aliases. Unknown strings pass through. main.rs:~4951 status JSON builder: reports resolved model. No source/raw/valid fields. Fix shape (~65 lines): - Reject empty string at parse time - Warn on unresolved aliases with fuzzy-match suggestion - Add model_source, model_raw to status JSON - Add model-validity check to doctor - Regression per failure mode Joins #105 (4-surface model disagreement) — model pair: #105 status ignores config model, doctor mislabels #124 --model flag unvalidated, no provenance in JSON Joins #122 (--base-commit zero validation) — unvalidated-flag pair: same parser pattern, no guards. Joins Silent-flag/documented-but-unenforced as 17th. Joins Truth-audit — status model field has no provenance. Joins Parallel-entry-point asymmetry as 10th. Filed in response to Clawhip pinpoint nudge 1495000973914144819 in #clawcode-building-in-public. |
||
|
|
bb76ec9730 |
ROADMAP #123: --allowedTools tool-name normalization asymmetric; snake_case canonicals accept variants, PascalCase canonicals reject snake_case; whitespace+comma split undocumented; allowed_tools not surfaced in JSON
Dogfooded 2026-04-18 on main HEAD 2bf2a11 from /tmp/cdZZ.
Asymmetric normalization:
normalize_tool_name(value) = trim + lowercase + replace -→_
Canonical 'read_file' (snake_case):
accepts: read_file, READ_FILE, Read-File, read-file,
Read (alias), read (alias)
rejects: ReadFile, readfile, READFILE
→ Because normalize('ReadFile')='readfile', and name_map
has key 'read_file' not 'readfile'.
Canonical 'WebFetch' (PascalCase):
accepts: WebFetch, webfetch, WEBFETCH
rejects: web_fetch, web-fetch, Web-Fetch
→ Because normalize('WebFetch')='webfetch' (no underscore).
User input 'web_fetch' normalizes to 'web_fetch' (keeps
underscore). Keys don't match.
The normalize function ADDS underscores (hyphen→underscore) but
DOESN'T REMOVE them. So PascalCase canonicals have underscore-
free normalized keys; user input with explicit underscores keeps
them, creating key mismatch.
Result: 'bash,Bash,BASH,Read,read_file,Read-File,WebFetch' all
accepted, but 'web_fetch,web-fetch' rejected.
Additional silent-flag issues:
- Splits on commas OR whitespace (undocumented — help says
TOOL[,TOOL...])
- 'bash,Bash,BASH' silently accepts all 3 case variants, no
dedup warning
- Allowed tools NOT in status/doctor JSON — claw passing
--allowedTools has no way to verify what runtime accepted
Trace:
tools/src/lib.rs:192-244 normalize_allowed_tools:
canonical_names from mvp_tool_specs + plugin_tools + runtime
name_map: (normalize_tool_name(canonical), canonical)
for token in value.split(|c| c==',' || c.is_whitespace()):
lookup normalize_tool_name(token) in name_map
tools/src/lib.rs:370-372 normalize_tool_name:
fn normalize_tool_name(value: &str) -> String {
value.trim().replace('-', '_').to_ascii_lowercase()
}
Replaces - with _. Lowercases. Does NOT remove _.
Asymmetry source: normalize('WebFetch')='webfetch',
normalize('web_fetch')='web_fetch'. Different keys.
--allowedTools NOT plumbed into Status JSON output
(no 'allowed_tools' field).
Fix shape (~50 lines):
- Symmetric normalization: strip underscores from both canonical
and input, OR don't normalize hyphens in input either.
Pick one convention.
- claw tools list / --allowedTools help subcommand that prints
canonical names + accepted variants.
- Surface allowed_tools in status/doctor JSON when flag set.
- Document comma+whitespace split semantics in --help.
- Warn on duplicate tokens (bash,Bash,BASH = 3 tokens, 1 unique).
- Regression per normalization pair + status surface + duplicate.
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116, #117, #118, #119, #121, #122) as 16th.
Joins Permission-audit/tool-allow-list (#94, #97, #101, #106,
#115, #120) as 7th.
Joins Truth-audit — status/doctor JSON hides what allowed-tools
set actually is.
Joins Parallel-entry-point asymmetry (#91, #101, #104, #105,
#108, #114, #117, #122) as 9th — --allowedTools vs
.claw.json permissions.allow likely disagree on normalization.
Natural bundles:
#97 + #123 — --allowedTools trust-gap pair:
empty silently blocks (#97) +
asymmetric normalization + invisible runtime state (#123)
Permission-audit 7-way (grown):
#94 + #97 + #101 + #106 + #115 + #120 + #123
Flagship permission-audit sweep 8-way (grown):
#50 + #87 + #91 + #94 + #97 + #101 + #115 + #123
Filed in response to Clawhip pinpoint nudge 1494993419536306176
in #clawcode-building-in-public.
|
||
|
|
2bf2a11943 |
ROADMAP #122: --base-commit greedy-consumes next arg with zero validation; subcommand/flag swallow; stale-base signal missing from status/doctor JSON surfaces
Dogfooded 2026-04-18 on main HEAD d1608ae from /tmp/cdYY.
Three related findings:
1. --base-commit has zero validation:
$ claw --base-commit doctor
warning: worktree HEAD (...) does not match expected
base commit (doctor). Session may run against a stale
codebase.
error: missing Anthropic credentials; ...
# 'doctor' used as base-commit value literally.
# Subcommand absorbed. Prompt fallthrough. Billable.
2. Greedy swallow of next flag:
$ claw --base-commit --model sonnet status
warning: ...does not match expected base commit (--model)
# '--model' taken as value. status never dispatched.
3. Garbage values silently accepted:
$ claw --base-commit garbage status
Status ...
# No validation. No warning (status path doesn't run check).
4. Stale-base signal missing from JSON surfaces:
$ claw --output-format json --base-commit $BASE status
{"kind":"status", ...}
# no stale_base, no base_commit, no base_commit_mismatch.
Stale-base check runs ONLY on Prompt path, as stderr prose.
Trace:
main.rs:487-494 --base-commit parsing:
'base-commit' => {
let value = args.get(index + 1).ok_or_else(...)?;
base_commit = Some(value.clone());
index += 2;
}
No format check. No reject-on-flag-prefix. No reject-on-
known-subcommand.
Compare main.rs:498-510 --reasoning-effort:
validates 'low' | 'medium' | 'high'. Has guard.
stale_base.rs check_base_commit runs on Prompt/turn path
only. No Status/Doctor handler includes base_commit field.
grep 'stale_base|base_commit_matches|base_commit:'
rust/crates/rusty-claude-cli/src/main.rs | grep status|doctor
→ zero matches.
Fix shape (~40 lines):
- Reject values starting with '-' (flag-like)
- Reject known-subcommand names as values
- Optionally run 'git cat-file -e {value}' to verify real commit
- Plumb base_commit + base_commit_matches + stale_base_warning
into Status and Doctor JSON surfaces
- Emit warning as structured JSON event too (not just stderr)
- Regression per failure mode
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116, #117, #118, #119, #121) as 15th.
Joins Parser-level trust gaps: #108 + #117 + #119 + #122 —
billable-token silent-burn via parser too-eager consumption.
Joins Parallel-entry-point asymmetry (#91, #101, #104, #105,
#108, #114, #117) as 8th — stale-base implemented for Prompt
but absent from Status/Doctor.
Joins Truth-audit — 'expected base commit (doctor)' lies by
including user's mistake as truth.
Cross-cluster with Unplumbed-subsystem (#78, #96, #100, #102,
#103, #107, #109, #111, #113, #121) — stale-base signal in
runtime but not JSON.
Natural bundles:
Parser-level trust gap quintet (grown):
#108 + #117 + #119 + #122 — billable-token silent-burn
via parser too-eager consumption.
#100 + #122 — stale-base diagnostic-integrity pair:
#100 stale-base subsystem unplumbed (general)
#122 --base-commit accepts anything, greedy, Status/Doctor
JSON unplumbed (specific)
Filed in response to Clawhip pinpoint nudge 1494978319920136232
in #clawcode-building-in-public.
|
||
|
|
d1608aede4 |
ROADMAP #121: hooks schema incompatible with Claude Code; error message misleading; doctor JSON emits 2 objects on failure breaking single-doc parsing; doctor has duplicate message+report fields
Dogfooded 2026-04-18 on main HEAD b81e642 from /tmp/cdWW.
Four related findings in one:
1. hooks schema incompatible with Claude Code (primary):
claw-code: {'hooks':{'PreToolUse':['cmd1','cmd2']}}
Claude Code: {'hooks':{'PreToolUse':[
{'matcher':'Bash','hooks':[{'type':'command','command':'...'}]}
]}}
Flat string array vs matcher-keyed object array. Incompatible.
User copying .claude.json hooks to .claw.json hits parse-fail.
2. Error message misleading:
'field hooks.PreToolUse must be an array of strings, got an array'
Both input and expected are arrays. Correct diagnosis:
'got an array of objects where array of strings expected'
3. Missing Claude Code hook event types:
claw-code supports: PreToolUse, PostToolUse, PostToolUseFailure
Claude Code supports: above + UserPromptSubmit, Notification,
Stop, SubagentStop, PreCompact, SessionStart
5+ event types missing.
matcher regex not supported.
type: 'command' vs type: 'http' extensibility not supported.
4. doctor NDJSON output on failures:
With failures present, --output-format json emits TWO
concatenated JSON objects on stdout:
Object 1: {kind:'doctor', has_failures:true, ...}
Object 2: {type:'error', error:'doctor found failing checks'}
python json.load() fails: 'Extra data: line 133 column 1'
Flag name 'json' violated — NDJSON is not JSON.
5. doctor message + report byte-duplicated:
.message and .report top-level fields have identical prose
content. Parser ambiguity + byte waste.
Trace:
config.rs:750-771 parse_optional_hooks_config_object:
optional_string_array(hooks, 'PreToolUse', context)
Expects ['cmd1', 'cmd2']. Claude Code gives
[{matcher,hooks:[{type,command}]}]. Schema-incompatible.
config.rs:775-779 validate_optional_hooks_config:
calls same parser. Error bubbles up.
Message comes from optional_string_array path —
technically correct but misleading.
Fix shape (~200 lines + migration docs):
- Dual-schema hooks parser: accept native + Claude Code forms
- Add missing event types to RuntimeHookConfig
- Implement matcher regex
- Fix error message to distinguish array-element types
- Fix doctor: single JSON object regardless of failure state
- De-duplicate message + report (keep report, drop message)
- Regression per schema form + event type + matcher
Joins Claude Code migration parity (#103, #109, #116, #117,
#119, #120) as 7th — most severe parity break since hooks is
load-bearing automation infrastructure.
Joins Truth-audit on misleading error message.
Joins Silent-flag on --output-format json emitting NDJSON.
Cross-cluster with Unplumbed-subsystem (#78, #96, #100, #102,
#103, #107, #109, #111, #113) — hooks subsystem exists but
schema incompatible with reference implementation.
Natural bundles:
Claude Code migration parity septet (grown flagship):
#103 + #109 + #116 + #117 + #119 + #120 + #121
Complete coverage of every migration failure mode.
#107 + #121 — hooks-subsystem pair:
#107 hooks invisible to JSON diagnostics
#121 hooks schema incompatible with migration source
Filed in response to Clawhip pinpoint nudge 1494963222157983774
in #clawcode-building-in-public.
|
||
|
|
b81e6422b4 |
ROADMAP #120: .claw.json custom JSON5-partial parser accepts trailing commas but silently drops comments/unquoted/BOM; combined with alias table 'default'→ReadOnly + no-config→DangerFullAccess creates security-critical user-intent inversion
Dogfooded 2026-04-18 on main HEAD 7859222 from /tmp/cdVV. Extends #86 (silent-drop general case) with two new angles: 1. JSON5-partial acceptance matrix: ACCEPTED (loaded correctly): - trailing comma (one) SILENTLY DROPPED (loaded_config_files=0, zero stderr, exit 0): - line comments (//) - block comments (/* */) - unquoted keys - UTF-8 BOM - single quotes - hex numbers - leading commas - multiple trailing commas 8 cases tested, 1 accepted, 7 silently dropped. The 1 accepted gives false signal of JSON5 tolerance. 2. Alias table creates user-intent inversion: config.rs:856-858: 'default' | 'plan' | 'read-only' => ReadOnly 'acceptEdits' | 'auto' | 'workspace-write' => WorkspaceWrite 'dontAsk' | 'danger-full-access' => DangerFullAccess CRITICAL: 'default' in the config file = ReadOnly no config at all = DangerFullAccess (per #87) These are OPPOSITE modes. Security-inversion chain: user writes: {'// comment', 'defaultMode': 'default'} user intent: read-only parser: rejects comment read_optional_json_object: silently returns Ok(None) config loader: no config present permission_mode: falls back to no-config default = DangerFullAccess ACTUAL RESULT: opposite of intent. ZERO warning. Trace: config.rs:674-692 read_optional_json_object: is_legacy_config = (file_name == '.claw.json') match JsonValue::parse(&contents) { Ok(parsed) => parsed, Err(_error) if is_legacy_config => return Ok(None), Err(error) => return Err(ConfigError::Parse(...)), } is_legacy silent-drop. (#86 covers general case) json.rs JsonValue::parse — custom parser: accepts trailing comma rejects everything else JSON5-ish Fix shape (~80 lines, overlaps with #86): - Pick policy: strict JSON or explicit JSON5. Enforce consistently. - Apply #86 fix here: replace silent-drop with warn-and-continue, structured warning in stderr + JSON surface. - Rename 'default' alias OR map to 'ask' (matches English meaning). - Structure status output: add config_parse_errors:[] field so claws detect silent drops via JSON without stderr-parsing. - Regression matrix per JSON5 feature + security-invariant test. Joins Permission-audit/tool-allow-list (#94, #97, #101, #106, #115) as 6th — this is the CONFIG-PARSE anchor of the permission- posture problem. Complete matrix: #87 absence → DangerFullAccess #101 env-var fail-OPEN → DangerFullAccess #115 init-generated dangerous default → DangerFullAccess #120 config parse-drops → DangerFullAccess Joins Truth-audit on loaded_config_files=0 + permission_mode= danger-full-access inconsistency without config_parse_errors[]. Joins Reporting-surface/config-hygiene (#90, #91, #92, #110, #115, #116) on silent-drop-no-stderr-exit-0 axis. Joins Claude Code migration parity (#103, #109, #116, #117, #119) as 6th — claw-code is strict-where-Claude-was-lax (#116) AND lax-where-Claude-was-strict (#120). Maximum migration confusion. Natural bundles: #86 + #120 — config-parse reliability pair: silent-drop general case (#86) + JSON5-partial-acceptance + alias-inversion (#120) Permission-drift-at-every-boundary 4-way: #87 + #101 + #115 + #120 — absence + env-var + init + config-drop. Complete coverage of every path to DangerFullAccess. Security-critical permission drift audit mega-bundle: #86 + #87 + #101 + #115 + #116 + #120 — five-way sweep of every path to wrong permissions. Filed in response to Clawhip pinpoint nudge 1494955670791913508 in #clawcode-building-in-public. |
||
|
|
78592221ec |
ROADMAP #119: claw <slash-only verb> + any arg silently falls through to Prompt; bare_slash_command_guidance gated by rest.len() != 1; 9 known verbs affected
Dogfooded 2026-04-18 on main HEAD 3848ea6 from /tmp/cdUU.
The 'this is a slash command' helpful-error only fires when
invoked EXACTLY bare. Adding ANY argument silently falls through
to Prompt dispatch and burns billable tokens.
$ claw --output-format json hooks
{"error":"`claw hooks` is a slash command. Use `claw
--resume SESSION.jsonl /hooks`..."}
# clean error
$ claw --output-format json hooks --help
{"error":"missing Anthropic credentials; ..."}
# Prompt fallthrough. The CLI tried to send 'hooks --help'
# to the LLM as a user prompt.
9 known slash-only verbs affected:
hooks, plan, theme, tasks, subagent, agent, providers,
tokens, cache
All exhibit identical pattern:
bare verb → clean error
verb + any arg (--help, list, on, off, --json, etc) →
Prompt fallthrough, billable LLM call
User pattern: 'claw status --help' prints usage. So users
naturally try 'claw hooks --help' expecting same. Gets
charged for prompt 'hooks --help' to LLM instead.
Trace:
main.rs:745-763 entry point:
if rest.len() != 1 { return None; } <-- THE BUG
match rest[0].as_str() {
'help' => ...,
'version' => ...,
other => bare_slash_command_guidance(other).map(Err),
}
main.rs:765-793 bare_slash_command_guidance:
looks up command in slash_command_specs()
returns helpful error string
WORKS CORRECTLY — just never called when args present
Claude Code convention: 'claude hooks --help' prints usage,
'claude hooks list' lists hooks. claw-code silently charges.
Compare sibling bugs:
#108 typo'd verb + args → Prompt (typo path)
#117 -p 'text' --arg → Prompt with swallowed flags (greedy -p)
#119 known slash-verb + any arg → Prompt (too-narrow guidance)
All three are silent-billable-token-burn. Same underlying cause:
too-narrow parser detection + greedy Prompt dispatch.
Fix shape (~35 lines):
- Remove rest.len() != 1 gate. Widen to:
if rest.is_empty() { return None; }
let first = rest[0].as_str();
if rest.len() == 1 {
// existing bare-verb handling
}
if let Some(guidance) = bare_slash_command_guidance(first) {
return Some(Err(format!(
'{} The extra argument `{}` was not recognized.',
guidance, rest[1..].join(' ')
)));
}
None
- Subcommand --help support: catch --help for all recognized
slash verbs, print SlashCommandSpec.description
- Regression tests: 'claw <verb> --help' prints help,
'claw <verb> any arg' prints guidance, no Prompt fallthrough
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116, #117, #118) as 14th.
Joins Claude Code migration parity (#103, #109, #116, #117)
as 5th — muscle memory from claude <verb> --help burns tokens.
Joins Truth-audit — 'missing credentials' is a lie; real cause
is CLI invocation was interpreted as chat prompt.
Cross-cluster with Parallel-entry-point asymmetry — slash-verb
with args is another entry point differing from bare form.
Natural bundles:
#108 + #117 + #119 — billable-token silent-burn triangle:
typo fallthrough (#108) +
flag swallow (#117) +
known-slash-verb fallthrough (#119)
#108 + #111 + #118 + #119 — parser-level trust gap quartet:
typo fallthrough + 2-way collapse + 3-way collapse +
known-verb fallthrough
Filed in response to Clawhip pinpoint nudge 1494948121099243550
in #clawcode-building-in-public.
|
||
|
|
3848ea64e3 |
ROADMAP #118: /stats, /tokens, /cache all collapse to SlashCommand::Stats; 3-way dispatch collapse with 3 distinct help descriptions
Dogfooded 2026-04-18 on main HEAD b9331ae from /tmp/cdTT.
Three slash commands collapse to one handler:
$ claw --help | grep -E '^\s*/(stats|tokens|cache)\s'
/stats Show workspace and session statistics [resume]
/tokens Show token count for the current conversation [resume]
/cache Show prompt cache statistics [resume]
Three distinct promises. One implementation:
$ claw --resume s --output-format json /stats
{"kind":"stats","input_tokens":0,"output_tokens":0,
"cache_creation_input_tokens":0,"cache_read_input_tokens":0,
"total_tokens":0}
$ claw --resume s --output-format json /tokens
{"kind":"stats", ...identical...}
$ claw --resume s --output-format json /cache
{"kind":"stats", ...identical...}
diff /stats /tokens → empty
diff /stats /cache → empty
kind field is always 'stats', never 'tokens' or 'cache'.
Trace:
commands/src/lib.rs:1405-1408:
'stats' | 'tokens' | 'cache' => {
validate_no_args(command, &args)?;
SlashCommand::Stats
}
commands/src/lib.rs:317 SlashCommandSpec name='stats' registered
commands/src/lib.rs:702 SlashCommandSpec name='tokens' registered
SlashCommandSpec name='cache' also registered
Each has distinct summary/description in help.
No SlashCommand::Tokens or SlashCommand::Cache variant exists.
main.rs:2872-2879 SlashCommand::Stats handler hard-codes
'kind': 'stats' regardless of which alias invoked.
More severe than #111:
#111: /providers → Doctor (2-way collapse, wildly wrong category)
#118: /stats + /tokens + /cache → Stats (3-way collapse with
THREE distinct advertised purposes)
The collapse hides information that IS available. /stats output
has cache_creation_input_tokens + cache_read_input_tokens as
top-level fields, so cache data is PRESENT. But /cache should
probably return {kind:'cache', cache_hits, cache_misses,
hit_rate}, a cache-specific schema. Similarly /tokens should
return {kind:'tokens', conversation_total, turns,
average_per_turn}. Implementation returns the union for all.
Fix shape (~90 lines):
- Add SlashCommand::Tokens and SlashCommand::Cache variants
- Parser arms:
'tokens' => SlashCommand::Tokens
'cache' => SlashCommand::Cache
'stats' => SlashCommand::Stats
- Handlers with distinct output schemas:
/tokens: {kind:'tokens', conversation_total, input_tokens,
output_tokens, turns, average_per_turn}
/cache: {kind:'cache', cache_creation_input_tokens,
cache_read_input_tokens, cache_hits, cache_misses,
hit_rate_pct}
/stats: {kind:'stats', subsystem:'all', ...}
- Regression per alias: kind matches, schema matches purpose
- Sweep parser for other collapse arms
- If aliasing intentional, annotate --help with (alias for X)
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116, #117) as 13th — more severe than #111.
Joins Truth-audit on help-vs-implementation mismatch axis.
Cross-cluster with Parallel-entry-point asymmetry on multiple-
surfaces-identical-implementation axis.
Natural bundles:
#111 + #118 — dispatch-collapse pair:
/providers → Doctor (2-way, wildly wrong)
/stats+/tokens+/cache → Stats (3-way, distinct purposes)
Complete parser-dispatch audit shape.
#108 + #111 + #118 — parser-level trust gaps:
typo fallthrough (#108) +
2-way collapse (#111) +
3-way collapse (#118)
Filed in response to Clawhip pinpoint nudge 1494940571385593958
in #clawcode-building-in-public.
|
||
|
|
b9331ae61b |
ROADMAP #117: -p flag is super-greedy, swallows all subsequent args into prompt; --help/--version/--model after -p silently consumed; flag-like prompts bypass emptiness check
Dogfooded 2026-04-18 on main HEAD f2d6538 from /tmp/cdSS.
-p (Claude Code compat shortcut) at main.rs:524-538:
"-p" => {
let prompt = args[index + 1..].join(" ");
if prompt.trim().is_empty() {
return Err(...);
}
return Ok(CliAction::Prompt {...});
}
args[index+1..].join(" ") = ABSORBS EVERY subsequent arg.
return Ok(...) = short-circuits parser, discards wants_help etc.
Failure modes:
1. Silent flag swallow:
claw -p "test" --model sonnet --output-format json
→ prompt = "test --model sonnet --output-format json"
→ model: default (not sonnet), format: text (not json)
→ LLM receives literal string '--model sonnet' as user input
→ billable tokens burned on corrupted prompt
2. --help/--version defeated:
claw -p "test" --help → sends 'test --help' to LLM
claw -p "test" --version → sends 'test --version' to LLM
claw --help -p "test" → wants_help=true set, then discarded
by -p's early return. Help never prints.
3. Emptiness check too weak:
claw -p --model sonnet
→ prompt = "--model sonnet" (non-empty)
→ passes is_empty() check
→ sends '--model sonnet' to LLM as the user prompt
→ no error raised
4. Flag-order invisible:
claw --model sonnet -p "test" → WORKS (model parsed first)
claw -p "test" --model sonnet → BROKEN (--model swallowed)
Same flags, different order, different behavior.
--help has zero warning about flag-order semantics.
Compare Claude Code:
claude -p "prompt" --model sonnet → works (model takes effect)
claw -p "prompt" --model sonnet → silently broken
Fix shape (~40 lines):
- "-p" takes exactly args[index+1] as prompt, continues parsing:
let prompt = args.get(index+1).cloned().unwrap_or_default();
if prompt.trim().is_empty() || prompt.starts_with('-') {
return Err("-p requires a prompt string");
}
pending_prompt = Some(prompt);
index += 2;
- Reject prompts that start with '-' unless preceded by '--':
'claw -p -- --literal-prompt' = literal '--literal-prompt'
- Consult wants_help before returning from -p branch.
- Regression tests:
-p "prompt" --model sonnet → model takes effect
-p "prompt" --help → help prints
-p --foo → error
--help -p "test" → help prints
-p -- --literal → literal prompt sent
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116) as 12th — -p is undocumented in --help
yet actively broken.
Joins Parallel-entry-point asymmetry (#91, #101, #104, #105,
#108, #114) as 7th — three entry points (prompt TEXT, bare
positional, -p TEXT) with subtly different arg-parsing.
Joins Claude Code migration parity (#103, #109, #116) as 4th —
users typing 'claude -p "..." --model ...' muscle memory get
silent prompt corruption.
Joins Truth-audit — parser lies about what it parsed.
Natural bundles:
#108 + #117 — billable-token silent-burn pair:
typo fallthrough burns tokens (#108) +
flag-swallow burns tokens (#117)
#105 + #108 + #117 — model-resolution triangle:
status ignores .claw.json model (#105) +
typo statuss burns tokens (#108) +
-p --model sonnet silently ignored (#117)
Filed in response to Clawhip pinpoint nudge 1494933025857736836
in #clawcode-building-in-public.
|
||
|
|
f2d653896d |
ROADMAP #116: unknown keys in .claw.json hard-fail startup with exit 1; Claude Code migration parity broken (apiKeyHelper rejected); forward-compat impossible; only first error surfaces
Dogfooded 2026-04-18 on main HEAD ad02761 from /tmp/cdRR.
Three related gaps in one finding:
1. Unknown keys are strict ERRORS, not warnings:
{"permissions":{"defaultMode":"default"},"futureField":"x"}
$ claw --output-format json status
# stdout: empty
# stderr: {"type":"error","error":"unknown key futureField"}
# exit: 1
2. Claude Code migration parity broken:
$ cp .claude.json .claw.json
# .claude.json has apiKeyHelper (real Claude Code field)
$ claw --output-format json status
# stderr: unknown key apiKeyHelper → exit 1
No 'this is a Claude Code field we don't support, ignored' message.
3. Only errors[0] is reported — iterative discovery required:
3 unknown fields → 3 edit-run-fix cycles to fix them all.
Error-routing split with --output-format json:
success → stdout
errors → stderr (structured JSON)
Empty stdout on config errors. A claw piping stdout silently
gets nothing. Must capture both streams.
No escape hatch. No --ignore-unknown-config, no --strict flag,
no strictValidation config option.
Trace:
config.rs:282-291 ConfigLoader gate:
let validation = validate_config_file(...);
if !validation.is_ok() {
let first_error = &validation.errors[0];
return Err(ConfigError::Parse(first_error.to_string()));
}
all_warnings.extend(validation.warnings);
config_validate.rs:19-47 DiagnosticKind::UnknownKey:
level: DiagnosticLevel::Error (not Warning)
config_validate.rs schema allow-list is hard-coded. No
forward-compat extension (no x-* reserved namespace, no
additionalProperties: true, no opt-in lax mode).
grep 'apiKeyHelper' rust/crates/runtime/ → 0 matches.
Claude-Code-native fields not tolerated as no-ops.
grep 'ignore.*unknown|--no-validate|strict.*validation'
rust/crates/ → 0 matches. No escape hatch.
Fix shape (~100 lines):
- Downgrade UnknownKey Error → Warning default. ~5 lines.
- Add strict mode flag: .claw.json strictValidation: true OR
--strict-config CLI flag. Default off. ~15 lines.
- Collect all diagnostics, don't halt on first. ~20 lines.
- TOLERATED_CLAUDE_CODE_FIELDS allow-list: apiKeyHelper, env
etc. emit migration-hint warning 'not yet supported; ignored'
instead of hard-fail. ~30 lines.
- Emit structured error envelope on stdout too, not just stderr.
--output-format json stdout includes config_diagnostics[]. ~15.
- Wire suggestion: Option<String> for UnknownKey via fuzzy
match ('permisions' → 'permissions'). ~15 lines.
- Regression tests per outcome.
Joins Claude Code migration parity (#103, #109) as 3rd member —
most severe migration break. #103 silently drops .md files,
#109 stderr-prose warnings, #116 outright hard-fails.
Joins Reporting-surface/config-hygiene (#90, #91, #92, #110,
#115) on error-routing-vs-stdout axis.
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115) — only first error reported, rest silent.
Cross-cluster with Truth-audit — validation.is_ok() hides all
but first structured problem.
Natural bundles:
#103 + #109 + #116 — Claude Code migration parity triangle:
loss of compat (.md dropped) +
loss of structure (stderr prose warnings) +
loss of forward-compat (unknowns hard-fail)
#109 + #116 — config validation reporting surface:
only first warning surfaces structurally (#109)
only first error surfaces structurally AND halts (#116)
Filed in response to Clawhip pinpoint nudge 1494925472239321160
in #clawcode-building-in-public.
|
||
|
|
ad02761918 |
ROADMAP #115: claw init hardcodes 'defaultMode: dontAsk' alias for danger-full-access; init output zero security signal; JSON wraps prose
Dogfooded 2026-04-18 on main HEAD ca09b6b from /tmp/cdPP.
Three compounding issues in one finding:
1. claw init generates .claw.json with dangerous default:
$ claw init && cat .claw.json
{"permissions":{"defaultMode":"dontAsk"}}
$ claw status | grep permission_mode
permission_mode: danger-full-access
2. The 'dontAsk' alias obscures the actual security posture:
config.rs:858 "dontAsk" | "danger-full-access" =>
Ok(ResolvedPermissionMode::DangerFullAccess)
User reads 'dontAsk' as 'skip confirmations I'd otherwise see'
— NOT 'grant every tool unconditional access'. But the two
parse identically. Alias name dilutes severity.
3. claw init --output-format json wraps prose in message field:
{
"kind": "init",
"message": "Init\n Project /private/tmp/cdPP\n
.claw/ created\n..."
}
Claws orchestrating setup must string-parse \n-prose to
know what got created. No files_created[], no
resolved_permission_mode, no security_posture.
Zero mention of 'danger', 'permission', or 'access' anywhere
in init output. The init report says 'Review and tailor the
generated guidance' — implying there's something benign to tailor.
Trace:
rusty-claude-cli/src/init.rs:4-9 STARTER_CLAW_JSON constant:
hardcoded {"permissions":{"defaultMode":"dontAsk"}}
runtime/src/config.rs:858 alias resolution:
"dontAsk" | "danger-full-access" => DangerFullAccess
rusty-claude-cli/src/init.rs:370 JSON-output also emits
'defaultMode': 'dontAsk' literal.
grep 'dontAsk' rust/crates/ → 4 matches. None explain that
dontAsk == danger-full-access anywhere user-facing.
Fix shape (~60 lines):
- STARTER_CLAW_JSON default → 'default' (explicit safe). Users
wanting danger-full-access opt in. ~5 lines.
- init output warns when effective mode is DangerFullAccess:
'security: danger-full-access (unconditional tool approval).'
~15 lines.
- Structure the init JSON:
{kind, files:[{path,action}], resolved_permission_mode,
permission_mode_source, security_warnings:[]}
~30 lines.
- Deprecate 'dontAsk' alias OR log warning at parse: 'alias for
danger-full-access; grants unconditional tool access'. ~8 lines.
- Regression tests per outcome.
Builds on #87 and amplifies it:
#87: absence-of-config default = danger-full-access
#101: fail-OPEN on bad RUSTY_CLAUDE_PERMISSION_MODE env var
#115: init actively generates the dangerous default
Three sequential compounding permission-posture failures.
Joins Permission-audit/tool-allow-list (#94, #97, #101, #106)
as 5th member — init-time anchor of the permission problem.
Joins Silent-flag/documented-but-unenforced on silent-setting
axis. Cross-cluster with Reporting-surface/config-hygiene
(prose-wrapped JSON) and Truth-audit (misleading 'Next step'
phrasing).
Natural bundle: #87 + #101 + #115 — 'permission drift at every
boundary': absence default + env-var bypass + init-generated.
Flagship permission-audit sweep grows 7-way:
#50 + #87 + #91 + #94 + #97 + #101 + #115
Filed in response to Clawhip pinpoint nudge 1494917922076889139
in #clawcode-building-in-public.
|
||
|
|
ca09b6b374 |
ROADMAP #114: /session list and --resume disagree after /clear; reported session_id unresumable; .bak files invisible; 0-byte files fabricate phantoms
Dogfooded 2026-04-18 on main HEAD 43eac4d from /tmp/cdNN and /tmp/cdOO.
Three related findings on session reference resolution asymmetry:
1. /clear divergence (primary):
- /clear --confirm rewrites session_id inside the file header
but reuses the old filename.
- /session list reads meta header, reports new id.
- --resume looks up by filename stem, not meta header.
- Net: /session list reports ids that --resume can't resolve.
Concrete:
claw --resume ses /clear --confirm
→ new_session_id: session-1776481564268-1
→ file still named ses.jsonl, meta session_id now the new id
claw --resume ses /session list
→ active: session-1776481564268-1
claw --resume session-1776481564268-1
→ ERROR session not found
2. .bak files filtered out of /session list silently:
ls .claw/sessions/<bucket>/
ses.jsonl ses.jsonl.before-clear-<ts>.bak
/session list → only ses.jsonl visible, .bak zero discoverability
is_managed_session_file only matches .jsonl and .json.
3. 0-byte session files fabricate phantom sessions:
touch .claw/sessions/<bucket>/emptyses.jsonl
claw --resume emptyses /session list
→ active: session-<ms>-0
→ sessions: [session-<ms>-1]
Two different fabricated ids, neither persisted to disk.
--resume either fabricated id → 'session not found'.
Trace:
session_control.rs:86-116 resolve_reference:
handle.id = session_id_from_path(&path) (filename stem)
.unwrap_or_else(|| ref.to_string())
Meta header NEVER consulted for ref → id mapping.
session_control.rs:118-137 resolve_managed_path:
for ext in [jsonl, json]:
path = sessions_root / '{ref}.{ext}'
if path.exists(): return
Lookup key is filename. Zero fallback to meta scan.
session_control.rs:228-285 collect_sessions_from_dir:
on load success: summary.id = session.session_id (meta)
on load failure: summary.id = path.file_stem() (filename)
/session list thus reports meta ids for good files.
/clear handler rewrites session_id in-place, writes to same
session_path. File keeps old name, gets new id inside.
is_managed_session_file filters .jsonl/.json only. .bak invisible.
Fix shape (~90 lines):
- /clear preserves filename's identity (Option A: keep session_id,
wipe content). /session fork handles new-id semantics (#113).
- resolve_reference falls back to meta-header scan when filename
lookup fails. Covers legacy divergent files.
- /session list surfaces backups via --include-backups flag OR
separate backups: [] array with structured metadata.
- 0-byte session files produce SessionError::EmptySessionFile
instead of silent fabrication. Structured error, not phantom.
- regression tests per failure mode.
Joins Session-handling: #93 + #112 + #113 + #114 — reference
resolution + concurrent-modification + programmatic management +
reference/enumeration asymmetry. Complete session-handling cluster.
Joins Truth-audit — /session list output factually wrong about
what is resumable.
Cross-cluster with Parallel-entry-point asymmetry (#91, #101,
#104, #105, #108) — entry points reading same underlying data
produce mutually inconsistent identifiers.
Natural bundle: #93 + #112 + #113 + #114 (session-handling
quartet — complete coverage).
Alternative bundle: #104 + #114 — /clear filename semantics +
/export filename semantics both hide identity in filename.
Filed in response to Clawhip pinpoint nudge 1494895272936079493
in #clawcode-building-in-public.
|
||
|
|
43eac4d94b |
ROADMAP #113: /session switch/fork/delete unsupported from --resume; no claw session CLI subcommand; REPL-only programmatic gap
Dogfooded 2026-04-18 on main HEAD 8b25daf from /tmp/cdJJ. Test matrix: /session list → works (structured JSON) /session switch s → 'unsupported resumed slash command' /session fork foo → 'unsupported resumed slash command' /session delete s → 'unsupported resumed slash command' /session delete s --force → 'unsupported resumed slash command' claw session delete s → Prompt fallthrough (#108), 'missing credentials' from LLM error path Help documents ALL session verbs as one unified capability: /session [list|switch <session-id>|fork [branch-name]|delete <session-id> [--force]] Summary: 'List, switch, fork, or delete managed local sessions' Implementation: main.rs:10618 parser builds SlashCommand::Session{action, target} for every subverb. All parse successfully. main.rs:2908-2925 dedicated /session list handler. Only one. main.rs:2936-2940+ catch-all: SlashCommand::Session {..} | SlashCommand::Plugins {..} | ... => Err(format_unsupported_resumed_slash_command(...)) main.rs:3963 SlashCommand::Session IS handled in LiveCli REPL path — switch/fork/delete implemented for interactive mode. runtime/session_control.rs:131+ SessionStore::resolve_reference, delete_managed_session, fork_managed_session all exist. grep 'claw session\b' main.rs → zero matches. No CLI subcommand. Gap: backing code exists, parser understands verbs, REPL handler wired — ONLY the --resume dispatch path lacks switch/fork/delete plumbing, and there's no claw session CLI subcommand as programmatic alternative. A claw orchestrating session lifecycle at scale has three options: a) start interactive REPL (impossible without TTY) b) manual .claw/sessions/ rm/cp (bypasses bookkeeping, breaks with #112's proposed locking) c) stick to /session list + /clear, accept missing verbs Fix shape (~130 lines): - /session switch <id> in run_resume_command (~25 lines) - /session fork [branch] in run_resume_command (~30 lines) - /session delete <id> [--force] in run_resume_command (~30), --force required without TTY - claw session <verb> CLI subcommand (~40) - --help: annotate which session verbs are resume-safe vs REPL-only - regression tests per verb x (CLI / slash-via-resume) Joins Unplumbed-subsystem (#78, #96, #100, #102, #103, #107, #109, #111) as 9th declared-but-not-delivered surface. Joins Session- handling (#93, #112) as 3rd member. Cross-cluster with Silent- flag on help-vs-impl mismatch. Natural bundles: #93 + #112 + #113 — session-handling triangle (semantic / concurrency / management API) #78 + #111 + #113 — declared-but-not-delivered triangle with three flavors: #78 fails-noisy (CLI variant → Prompt fallthrough) #111 fails-quiet (slash → wrong handler) #113 no-handler-at-all (slash → unsupported-resumed) Filed in response to Clawhip pinpoint nudge 1494887723818029156 in #clawcode-building-in-public. |
||
|
|
8b25daf915 |
ROADMAP #112: concurrent /compact and /clear race with raw 'No such file or directory (os error 2)' on session file
Dogfooded 2026-04-18 on main HEAD a049bd2 from /tmp/cdII.
5 concurrent /compact on same session → 4 succeed, 1 races with
raw ENOENT. Same pattern with concurrent /clear --confirm.
Trace:
session.rs:204-212 save_to_path:
rotate_session_file_if_needed(path)?
write_atomic(path, &snapshot)?
cleanup_rotated_logs(path)?
Three steps. No lock around sequence.
session.rs:1085-1094 rotate_session_file_if_needed:
metadata(path) → rename(path, rot_path)
Classic TOCTOU. Race window between check and rename.
session.rs:1063-1071 write_atomic:
writes .tmp-{ts}-{counter}, renames to path
Atomic per rename, not per multi-step sequence.
cleanup_rotated_logs deletes .rot-{ts} files older than 3 most
recent. Can race against another process reading that rot file.
No flock, no advisory lock file, no fcntl.
grep 'flock|FileLock|advisory' session.rs → zero matches.
SessionError::Io Display forwards os::Error Display:
'No such file or directory (os error 2)'
No domain translation to 'session file vanished during save'
or 'concurrent modification detected, retry safe'.
Fix shape (~90 lines + test):
- advisory lock: .claw/sessions/<bucket>/<session>.jsonl.lock
exclusive flock for duration of save_to_path (fs2 crate)
- domain error variants:
SessionError::ConcurrentModification {path, operation}
SessionError::SessionFileVanished {path}
- error-to-JSON mapping:
{error_kind: 'concurrent_modification', retry_safe: true}
- retry-policy hints on idempotent ops (/compact, /clear)
- regression test: spawn 10 concurrent /compact, assert all
success OR structured ConcurrentModification (no raw os_error)
Affected operations:
- /compact (session save_to_path after compaction)
- /clear --confirm (save_to_path after new session)
- /export (may hit rotation boundary)
- Turn-persist (append_persisted_message can race rotation)
Not inherently a bug if sessions are single-writer, but
workspace-bucket scoping at session_control.rs:31-32 assumes
one claw per workspace. Parallel ulw lanes, CI matrix runners,
orchestration loops all violate that assumption.
Joins truth-audit (error lies by omission about what happened).
New micro-cluster 'session handling' with #93. Adjacent to
#104 on session-file-handling axis.
Natural bundle: #93 + #112 (session semantic correctness +
concurrency error clarity).
Filed in response to Clawhip pinpoint nudge 1494880177099116586
in #clawcode-building-in-public.
|
||
|
|
a049bd29b1 |
ROADMAP #111: /providers documented as 'List available model providers' but dispatches to Doctor
Dogfooded 2026-04-18 on main HEAD b2366d1 from /tmp/cdHH.
Specification mismatch at the command-dispatch layer:
commands/src/lib.rs:716-720 SlashCommandSpec registry:
name: 'providers', summary: 'List available model providers'
commands/src/lib.rs:1386 parser:
'doctor' | 'providers' => SlashCommand::Doctor
So /providers dispatches to SlashCommand::Doctor. A claw calling
/providers expecting {kind: 'providers', providers: [...]} gets
{kind: 'doctor', checks: [auth, config, install_source, workspace,
sandbox, system]} instead. Same top-level kind field name,
completely different payload.
Help text lies twice:
--help slash listing: '/providers List available model providers'
--help Resume-safe summary: includes /providers
Unlike STUB_COMMANDS (#96) which fail noisily, /providers fails
QUIETLY — returns wrong subsystem output.
Runtime has provider data:
ProviderKind::{Anthropic, Xai, OpenAi, ...} at main.rs:1143-1147
resolve_repl_model with provider-prefix routing
pricing_for_model with per-provider costs
provider_fallbacks config field
Scaffolding is present; /providers just doesn't use it.
By contrast /tokens → Stats and /cache → Stats are semantically
reasonable (Stats has the requested data). /providers → Doctor
is genuinely bizarre.
Fix shape:
A. Implement: SlashCommand::Providers variant + render helper
using ProviderKind + provider_fallbacks + env-var check (~60)
B. Remove: delete 'providers' from registry + parser (~3 lines)
then /providers becomes 'unknown, did you mean /doctor?'
Either way: fix --help to match.
Parallel to #78 (claw plugins CLI variant never constructed,
falls through to prompt). Both are 'declared in spec, not
implemented as declared.' #78 fails noisy, #111 fails quiet.
Joins silent-flag cluster (#96-#101, #104, #108) — 8th
doc-vs-impl mismatch. Joins unplumbed-subsystem (#78, #96,
#100, #102, #103, #107, #109) as 8th declared-but-not-
delivered surface. Joins truth-audit.
Natural bundles:
#78 + #96 + #111 — declared-but-not-as-declared triangle
#96 + #108 + #111 — full --help/dispatch hygiene quartet
(help-filter-leaks + subcommand typo fallthrough + slash
mis-dispatch)
Filed in response to Clawhip pinpoint nudge 1494872623782301817
in #clawcode-building-in-public.
|
||
|
|
b2366d113a |
ROADMAP #110: ConfigLoader only checks cwd paths; .claw.json at project_root invisible from subdirectories
Dogfooded 2026-04-18 on main HEAD 16244ce from /tmp/cdGG/nested/deep/dir.
ConfigLoader::discover at config.rs:242-270 hardcodes every
project/local path as self.cwd.join(...):
- self.cwd.join('.claw.json')
- self.cwd.join('.claw').join('settings.json')
- self.cwd.join('.claw').join('settings.local.json')
No ancestor walk. No consultation of project_root.
Concrete:
cd /tmp/cdGG && git init && echo '{permissions:{defaultMode:read-only}}' > .claw.json
cd /tmp/cdGG/nested/deep/dir
claw status → permission_mode: 'danger-full-access' (fallback)
claw doctor → 'Config files loaded 0/0, defaults are active'
But project_root: /tmp/cdGG is correctly detected via git walk.
Same config file, same repo, invisible from subdirectory.
Meanwhile CLAUDE.md discovery walks ancestors unbounded (per #85
over-discovery). Same subsystem category, opposite policy, no doc.
Security-adjacent per #87: permission-mode fallback is
danger-full-access. cd'ing to a subdirectory silently upgrades
from read-only (configured) → danger-full-access (fallback) —
workspace-location-dependent permission drift.
Fix shape (~90 lines):
- add project_root_for(&cwd) helper (reuse git-root walker from
render_doctor_report)
- config search: user → project_root/.claw.json →
project_root/.claw/settings.json → cwd/.claw.json (overlay) →
cwd/.claw/settings.* (overlays)
- optionally walk intermediate ancestors
- surface 'where did my config come from' in doctor (pairs with
#106 + #109 provenance)
- warn when cwd has no config but project_root does
- documentation parity with CLAUDE.md
- regression tests per cwd depth + overlay precedence
Joins truth-audit (doctor says 'ok, defaults active' when config
exists). Joins discovery-overreach as opposite-direction sibling:
#85: skills ancestor walk UNBOUNDED (over-discovery)
#88: CLAUDE.md ancestor walk enables injection
#110: config NO ancestor walk (under-discovery)
Natural bundle: #85 + #110 (ancestor policy unification), or
#85 + #88 + #110 (full three-way ancestor-walk audit).
Filed in response to Clawhip pinpoint nudge 1494865079567519834
in #clawcode-building-in-public.
|
||
|
|
16244cec34 |
ROADMAP #109: config validation warnings stderr-only; structured ConfigDiagnostic flattened to prose, JSON-invisible
Dogfooded 2026-04-18 on main HEAD 21b2773 from /tmp/cdDD.
Validator produces structured diagnostics but loader discards
them after stderr eprintln:
config_validate.rs:19-66 ConfigDiagnostic {path, field, line,
kind: UnknownKey|WrongType|Deprecated}
config_validate.rs:313-322 DEPRECATED_FIELDS: permissionMode,
enabledPlugins
config_validate.rs:451 emits DiagnosticKind::Deprecated
config.rs:285-300 ConfigLoader::load:
if !validation.is_ok() {
return Err(validation.errors[0].to_string()) // ERRORS propagate
}
all_warnings.extend(validation.warnings);
for warning in &all_warnings {
eprintln!('warning: {warning}'); // WARNINGS stderr only
}
RuntimeConfig has no warnings field. No accessor. No route from
validator structured data to doctor/status JSON envelope.
Concrete:
.claw.json with enabledPlugins:{foo:true}
→ config check: {status: 'ok', summary: 'runtime config
loaded successfully'}
→ stderr: 'warning: field enabledPlugins is deprecated'
→ claw with 2>/dev/null loses the warning entirely
Errors DO propagate correctly:
.claw.json with 'permisions' (typo)
→ config check: {status: 'fail', summary: 'unknown key
permisions... Did you mean permissions?'}
Warning→stderr, Error→JSON asymmetry: a claw reading JSON can
see errors structurally but can't see warnings at all. Silent
migration drift: legacy claude-code 'permissionMode' key still
works, warning lost, operator never sees 'use permissions.
defaultMode' guidance unless they notice stderr.
Fix shape (~85 lines, all additive):
- add warnings: Vec<ConfigDiagnostic> field to RuntimeConfig
- populate from all_warnings, keep eprintln for human ops
- add ConfigDiagnostic::to_json_value emitting
{path, field, line, kind, message, replacement?}
- check_config_health: status='warn' + warnings[] JSON when
non-empty
- surface in status JSON (config_warnings[] or top-level
warnings[])
- surface in /config slash-command output
- regression tests per deprecated field + aggregation + no-warn
Joins truth-audit (#80-#87, #89, #100, #102, #103, #105, #107)
— doctor says 'ok' while validator flagged deprecations. Joins
unplumbed-subsystem (#78, #96, #100, #102, #103, #107) — 7th
surface. Joins Claude Code migration parity (#103) —
permissionMode legacy path is stderr-only.
Natural bundles:
#100 + #102 + #103 + #107 + #109 — 5-way doctor-surface
coverage plus structured warnings (doctor stops lying PR)
#107 + #109 — stderr-only-prose-warning sweep (hook events +
config warnings = same plumbing pattern)
Filed in response to Clawhip pinpoint nudge 1494857528335532174
in #clawcode-building-in-public.
|
||
|
|
21b2773233 |
ROADMAP #108: subcommand typos silently fall through to LLM prompt dispatch, burning billed tokens
Dogfooded 2026-04-18 on main HEAD 91c79ba from /tmp/cdCC.
Unrecognized first-positional tokens fall through the
_other => Ok(CliAction::Prompt { ... }) arm at main.rs:707.
Per --help this is 'Shorthand non-interactive prompt mode' —
documented behavior — but it eats known-subcommand typos too:
claw doctorr → Prompt("doctorr") → LLM API call
claw skilsl → Prompt("skilsl") → LLM API call
claw statuss → Prompt("statuss") → LLM API call
claw deply → Prompt("deply") → LLM API call
With credentials set, each burns real tokens. Without creds,
returns 'missing Anthropic credentials' — indistinguishable
from a legitimate prompt failure. No 'did you mean' suggestion.
Infrastructure exists:
slash command typos:
claw --resume s /skilsl
→ 'Unknown slash command: /skilsl. Did you mean /skill, /skills'
flag typos:
claw --fake-flag
→ structured error 'unknown option: --fake-flag'
subcommand typos:
→ silently become LLM prompts
The did-you-mean helper exists for slash commands. Flag
validation exists. Only subcommand dispatch has the silent-
fallthrough.
Fix shape (~60 lines):
- suggest_similar_subcommand(token) using levenshtein ≤ 2
against the ~16-item known-subcommand list
- gate the Prompt fallthrough on a shape heuristic:
single-token + near-match → return structured error with
did-you-mean. Otherwise fall through unchanged.
- preserve shorthand-prompt mode for multi-word inputs,
quoted inputs, and non-near-match tokens
- regression tests per typo shape + legit prompt + quoted
workaround
Cross-claw orchestration hazard: claws constructing subcommand
names from config or other claws' output have a latent 'typo →
live LLM call' vector. Over CI matrix with 1% typo rate, that's
billed-token waste + structural signal loss (error handler
can't distinguish typo from legit prompt failure).
Joins silent-flag cluster (#96-#101, #104) on subcommand axis —
6th instance of 'malformed input silently produces unintended
behavior.' Joins parallel-entry-point asymmetry (#91, #101,
#104, #105) — slash vs subcommand disagree on typo handling.
Natural bundles: #96 + #98 + #108 (--help/dispatch surface
hygiene triangle), #91 + #101 + #104 + #105 + #108 (parallel-
entry-point 5-way).
Filed in response to Clawhip pinpoint nudge 1494849975530815590
in #clawcode-building-in-public.
|
||
|
|
91c79baf20 |
ROADMAP #107: hooks subsystem fully invisible to JSON diagnostic surfaces; doctor no hook check, /hooks is stub, progress events stderr-only
Dogfooded 2026-04-18 on main HEAD a436f9e from /tmp/cdBB. Complete hook invisibility across JSON diagnostic surfaces: 1. doctor: no check_hooks_health function exists. check_config_health emits 'Config files loaded N/M, MCP servers N, Discovered file X' — NO hook count, no hook event breakdown, no hook health. .claw.json with 3 hooks (including /does/not/exist and curl-pipe-sh remote-exec payload) → doctor: ok, has_failures: false. 2. /hooks list: in STUB_COMMANDS (main.rs:7272) → returns 'not yet implemented in this build'. Parallel /mcp list / /agents list / /skills list work fine. /hooks has no sibling. 3. /config hooks: reports loaded_files and merged_keys but NOT hook bodies, NOT hook source files, NOT per-event breakdown. 4. Hook progress events route to eprintln! as prose: CliHookProgressReporter (main.rs:6660-6695) emits '[hook PreToolUse] tool_name: command' to stderr unconditionally. NEVER into --output-format json. A claw piping stderr to /dev/null (common in pipelines) loses all hook visibility. 5. parse_optional_hooks_config_object (config.rs:766) accepts any non-empty string. No fs::metadata() check, no which() check, no shell-syntax sanity check. 6. shell_command (hooks.rs:739-754) runs 'sh -lc <command>' with full shell expansion — env vars, globs, pipes, , remote curl pipes. Compounds with #106: downstream .claw/settings.local.json can silently replace the entire upstream hook array via the deep_merge_objects replace-semantic. A team-level audit hook in ~/.claw/settings.json is erasable and replaceable by an attacker-controlled hook with zero visibility anywhere machine-readable. Fix shape (~220 lines, all additive): - check_hooks_health doctor check (like #102's check_mcp_health) - status JSON exposes {pre_tool_use, post_tool_use, post_tool_use_failure} with source-file provenance - implement /hooks list (remove from STUB_COMMANDS) - route HookProgressEvent into JSON turn-summary as hook_events[] - validate hook commands at config-load, classify execution_kind - regression tests Joins truth-audit (#80-#87, #89, #100, #102, #103, #105) — doctor lies when hooks are broken or hostile. Joins unplumbed-subsystem (#78, #96, #100, #102, #103) — HookProgressEvent exists, JSON-invisible. Joins subsystem-doctor-coverage (#100, #102, #103) as fourth opaque subsystem. Cross-cluster with permission-audit (#94, #97, #101, #106) because hooks ARE a permission mechanism. Natural bundle: #102 + #103 + #107 (subsystem-doctor-coverage 3-way becomes 4-way). Plus #106 + #107 (policy-erasure + policy- visibility = complete hook-security story). Filed in response to Clawhip pinpoint nudge 1494834879127486544 in #clawcode-building-in-public. |
||
|
|
a436f9e2d6 |
ROADMAP #106: config merge deep_merge_objects REPLACES arrays; permission deny rules can be silently erased by downstream config layer
Dogfooded 2026-04-18 on main HEAD 71e7729 from /tmp/cdAA.
deep_merge_objects at config.rs:1216-1230 recurses into nested
objects but REPLACES arrays. So:
~/.claw/settings.json: {"permissions":{"deny":["Bash(rm *)"]}}
.claw.json: {"permissions":{"deny":["Bash(sudo *)"]}}
Merged: {"permissions":{"deny":["Bash(sudo *)"]}}
User's Bash(rm *) deny rule SILENTLY LOST. No warning. doctor: ok.
Worst case:
~/.claw/settings.json: {deny: [...strict list...]}
.claw/settings.local.json: {deny: []}
Merged: {deny: []}
Every deny rule from every upstream layer silently removed by a
workspace-local file. Any team/org security policy distributed
via user-home config is trivially erasable.
Arrays affected:
permissions.allow/deny/ask
hooks.PreToolUse/PostToolUse/PostToolUseFailure
plugins.externalDirectories
MCP servers are merged BY-KEY (merge_mcp_servers at :709) so
distinct server names across layers coexist. Author chose
merge-by-key for MCP but not for policy arrays. Design is
internally inconsistent.
extend_unique + push_unique helpers EXIST at :1232-1244 that do
union-merge with dedup. They are not called on the config-merge
axis for any policy array.
Fix shape (~100 lines):
- union-merge permissions.allow/deny/ask via extend_unique
- union-merge hooks.* arrays
- union-merge plugins.externalDirectories
- explicit replace-semantic opt-in via 'deny!' sentinel or
'permissions.replace: [...]' form (opt-in, not default)
- doctor surfaces policy provenance per rule (also helps #94)
- emit warning when replace-sentinel is used
- regression tests for union + explicit replace + multi-layer
Joins permission-audit sweep as 4-way composition-axis finding
(#94, #97, #101, #106). Joins truth-audit (doctor says 'ok'
while silently deleted every deny rule).
Natural bundle: #94 + #106 (rule validation + rule composition).
Plus #91 + #94 + #97 + #101 + #106 as 5-way policy-surface-audit.
Filed in response to Clawhip pinpoint nudge 1494827325085454407
in #clawcode-building-in-public.
|
||
|
|
71e77290b9 |
ROADMAP #105: claw status ignores .claw.json model, doctor mislabels alias as Resolved, 4 surfaces disagree
Dogfooded 2026-04-18 on main HEAD 6580903 from /tmp/cdZ.
.claw.json with {"model":"haiku"} produces:
claw status → model: 'claude-opus-4-6' (DEFAULT_MODEL, config ignored)
claw doctor → 'Resolved model haiku' (raw alias, label lies)
turn dispatch → claude-haiku-4-5-20251213 (actually-resolved canonical)
ANTHROPIC_MODEL=sonnet → status still says claude-opus-4-6
FOUR separate understandings of 'active model':
1. config file (alias as written)
2. doctor (alias mislabeled as 'Resolved')
3. status (hardcoded DEFAULT_MODEL ignoring config entirely)
4. turn dispatch (canonical, alias-resolved, what turns actually use)
Trace:
main.rs:59 DEFAULT_MODEL const = claude-opus-4-6
main.rs:400 parse_args starts model = DEFAULT_MODEL
main.rs:753 Status dispatch: model.to_string() — never calls
resolve_repl_model, never reads config or env
main.rs:1125 resolve_repl_model: source of truth for actual
model, consults ANTHROPIC_MODEL env + config + alias table.
Called from Prompt and Repl dispatch. NOT from Status.
main.rs:1701 check_config_health: 'Resolved model {model}'
where model is raw configured string, not resolved.
Label says Resolved, value is pre-resolution alias.
Orchestration hazard: a claw picks tool strategy based on
status.model assuming it reflects what turns will use. Status
lies: always reports DEFAULT_MODEL unless --model flag was
passed. Config and env var completely ignored by status.
Fix shape (~30 lines):
- call resolve_repl_model from print_status_snapshot
- add effective_model field to status JSON (or rename/enrich)
- fix doctor 'Resolved model' label (either rename to 'Configured'
or actually alias-resolve before emitting)
- honor ANTHROPIC_MODEL env in status
- regression tests per model source with cross-surface equality
Joins truth-audit (#80-#84, #86, #87, #89, #100, #102, #103).
Joins two-paths-diverge (#91, #101, #104) — now 4-way with #105.
Joins doctor-surface-coverage triangle (#100 + #102 + #105).
Filed in response to Clawhip pinpoint nudge 1494819785676947543
in #clawcode-building-in-public.
|
||
|
|
6580903d20 |
ROADMAP #104: /export and claw export are two paths with incompatible filename semantics; slash silently .txt-rewrites
Dogfooded 2026-04-18 on main HEAD 7447232 from /tmp/cdY.
Two-path-diverge problem:
A. /export slash command (resolve_export_path at main.rs:5990-6010):
- If extension != 'txt', silently appends '.txt'
- /export foo.md → writes foo.md.txt
- /export report.json → writes report.json.txt
- cwd.join(relative_path_with_dotdot) resolves outside cwd
- No path-traversal rejection
B. claw export CLI (run_export at main.rs:6021-6055):
- fs::write(path, &markdown) directly, no suffix munging
- /tmp/cli-export.md → writes /tmp/cli-export.md
- Also no path-traversal check, absolute paths write wherever
Same logical action, incompatible output contracts. A claw that
switches between /export and claw export sees different output
filenames for the same input.
Compounded:
- Content is Markdown (render_session_markdown emits '# Conversation
Export', '## 1. User', fenced code blocks) but slash path forces
.txt extension → content/extension mismatch. File-routing
pipelines (archival by extension, syntax highlight, preview)
misclassify.
- --help says just '/export [file]'. No mention of .txt forcing,
no mention of path-resolution semantics.
- Claw pipelines that glob *.md won't find /export outputs.
Trace:
main.rs:5990 resolve_export_path: extension check + conditional
.txt append
main.rs:6021 run_export: fs::write direct, no path munging
main.rs:5975 default_export_filename: hardcodes .txt fallback
Content renderer is Markdown (render_session_markdown:6075)
Fix shape (~70 lines):
- unify both paths via shared export_session_to_path helper
- respect caller's extension (pick renderer by extension or
accept that content is Markdown and name accordingly)
- path-traversal policy decision: restrict to project root or
allow-with-warning
- --help: document suffix preservation + path semantics
- regression tests for extension preservation + dotdot rejection
Joins silent-flag cluster (#96-#101) on silent-rewrite axis.
New two-paths-diverge sub-cluster: #91 (permission-mode parser
disagree) + #101 (CLI vs env asymmetry) + #104 (slash vs CLI
export asymmetry) — three instances of parallel entry points
doing subtly different things.
Natural bundles: #91 + #101 + #104 (two-paths-diverge trio),
#96 + #98 + #99 + #101 + #104 (silent-rewrite-or-noop quintet).
Filed in response to Clawhip pinpoint nudge 1494812230372294849
in #clawcode-building-in-public.
|
||
|
|
7447232688 |
ROADMAP #103: claw agents silently drops every non-.toml file; claude-code convention .md files ignored, no content validation
Dogfooded 2026-04-18 on main HEAD 6a16f08 from /tmp/cdX. Two-part gap on agent subsystem: 1. File-format gate silently discards .md (YAML frontmatter): commands/src/lib.rs:3180-3220 load_agents_from_roots filters extension() != 'toml' and silently continues. No log, no warn. .claw/agents/foo.md → agents list count: 0, doctor: ok. Same file renamed to .toml → discovered instantly. 2. No content validation inside accepted .toml: model='nonexistent/model-that-does-not-exist' → accepted. tools=['DoesNotExist', 'AlsoFake'] → accepted. reasoning_effort string → unvalidated. No check against model registry, tool registry, or reasoning-effort enum — all machinery exists elsewhere (#97 validates tools for --allowedTools flag). Compounded: - agents help JSON lists sources but NOT accepted file formats. Operators have zero documentation-surface way to diagnose 'why does my .md file not work?' - Doctor check set has no agents check. 3 files present with 1 silently skipped → summary: 'ok'. - Skills use .md (SKILL.md). MCP uses .json (.claw.json). Agents uses .toml. Three subsystems, three formats, no cross-subsystem consistency or documentation. - Claude Code convention is .md with YAML frontmatter. Migrating operators copy that and silently fail. Fix shape (~100 lines): - accept .md with YAML frontmatter via existing parse_skill_frontmatter helper - validate model/tools/reasoning_effort against existing registries; emit status: 'invalid' + validation_errors instead of silently accepting - agents list summary.skipped: [{path, reason}] - add agents doctor check (total/active/skipped/invalid) - agents help: accepted_formats list Joins truth-audit (#80-#84, #86, #87, #89, #100, #102) on silent-ok-while-ignoring axis. Joins silent-flag (#96-#101) at subsystem scale. Joins unplumbed-subsystem (#78, #96, #100, #102) as 5th unreachable surface: load_agents_from_roots present, parse_skill_frontmatter present, validation helpers present, agents path calls none of them. Also opens new 'Claude Code migration parity' cross-cluster: claw-code silently breaks the expected convention migration path for a first-class subsystem. Natural bundles: #102 + #103 (subsystem-doctor-coverage), #78 + #96 + #100 + #102 + #103 (unplumbed-surface quintet). Filed in response to Clawhip pinpoint nudge 1494804679962661187 in #clawcode-building-in-public. |
||
|
|
6a16f0824d |
ROADMAP #102: mcp list/show/doctor surface MCP config-time only; no preflight, no liveness, not even command-exists check
Dogfooded 2026-04-18 on main HEAD eabd257 from /tmp/cdW2.
A .claw.json pointing at command='/does/not/exist' as an MCP server
cheerfully reports:
mcp show unreachable → found: true
mcp list → configured_servers: 1, status field absent
doctor → config: ok, MCP servers: 1, has_failures: false
The broken server is invisible until agent tries to call a tool
from it mid-turn — burning tokens on failed tool call and forcing
retry loop.
Trace:
main.rs:1701-1780 check_config_health counts via
runtime_config.mcp().servers().len()
No which(). No TcpStream::connect(). No filesystem touch.
render_doctor_report has 6 checks (auth/config/install_source/
workspace/sandbox/system). No check_mcp_health exists.
commands/src/lib.rs mcp list/show emit config-side repr only.
No status field, no reachable field, no startup_state.
runtime/mcp_stdio.rs HAS startup machinery with error types,
but only invoked at turn-execution time — too late for
preflight.
Roadmap prescribes this exact surface:
- Phase 1 §3.5 Boot preflight / doctor contract explicitly lists
'MCP config presence and server reachability expectations'
- Phase 2 §4 canonical lane event schema includes lane.ready
- Phase 4.4.4 event provenance / environment labeling
- Product Principle #5 'Partial success is first-class' —
'MCP startup can succeed for some servers and fail for
others, with structured degraded-mode reporting'
All four unimplementable without preflight + per-server status.
Fix shape (~110 lines):
- check_mcp_health: which(command) for stdio, 1s TcpStream
connect for http/sse. Aggregate ok/warn/fail with per-server
detail lines.
- mcp list/show: add status field
(configured/resolved/command_not_found/connect_refused/
startup_failed). --probe flag for deeper handshake.
- doctor top-level: degraded_mode: bool, startup_summary.
- Wire preflight into prompt/repl bootstrap; emit one-time
mcp_preflight event.
Joins unplumbed-subsystem cross-cluster (#78, #100, #102) —
subsystem exists, diagnostic surface JSON-invisible. Joins
truth-audit (#80-#84, #86, #87, #89, #100) — doctor: ok lies
when MCP broken.
Natural bundle: #78 + #96 + #100 + #102 unplumbed-surface
quartet. Also #100 + #102 as pure doctor-surface-coverage 2-way.
Filed in response to Clawhip pinpoint nudge 1494797126041862285
in #clawcode-building-in-public.
|
||
|
|
eabd257968 |
ROADMAP #101: RUSTY_CLAUDE_PERMISSION_MODE env var silently fails OPEN to danger-full-access on any invalid value
Dogfooded 2026-04-18 on main HEAD d63d58f from /tmp/cdV.
Qualitatively worse than #96-#100 silent-flag class because this
is fail-OPEN, not fail-inert: operator intent 'restrict this lane'
silently becomes 'full access.'
Tested matrix:
VALID → correct mode:
read-only → read-only
workspace-write → workspace-write
danger-full-access → danger-full-access
' read-only ' → read-only (trim works)
INVALID → silent danger-full-access:
'' → danger-full-access
'readonly' → danger-full-access (typo: missing hyphen)
'read_only' → danger-full-access (typo: underscore)
'READ-ONLY' → danger-full-access (case)
'ReadOnly' → danger-full-access (case)
'dontAsk' → danger-full-access (config alias not recognized by env parser, but ultimate default happens to be dfa)
'garbage' → danger-full-access (pure garbage)
'readonly\n' → danger-full-access
CLI asymmetry: --permission-mode readonly → loud structured error.
Same misspelling, same input, opposite outcomes via env vs CLI.
Trace:
main.rs:1099-1107 default_permission_mode:
env::var(...).ok().and_then(normalize_permission_mode)
.or_else(config...).unwrap_or(DangerFullAccess)
→ .and_then drops error context on invalid;
.unwrap_or fail-OPEN to most permissive mode
main.rs:5455-5462 normalize_permission_mode accepts 3 canonical;
runtime/config.rs:855-863 parse_permission_mode_label accepts 7
including config aliases (default/plan/acceptEdits/auto/dontAsk).
Two parsers, disagree on accepted set, no shared source of truth.
Plus: env var RUSTY_CLAUDE_PERMISSION_MODE is UNDOCUMENTED.
grep of README/docs/help returns zero hits.
Fix shape (~60 lines total):
- rewrite default_permission_mode to surface invalid values via Result
- share ONE parser across CLI/config/env (extract from config.rs:855)
- decide broad (7 aliases) vs narrow (3 canonical) accepted set
- document the env var in --help Environment section
- add doctor check surfacing permission_mode.source attribution
- optional: rename to CLAW_PERMISSION_MODE with deprecation alias
Joins permission-audit sweep (#50/#87/#91/#94/#97/#101) on the env
axis. Completes the three-way input-surface audit: CLI + config +
env. Cross-cluster with silent-flag #96-#100 (worse variant: fail-OPEN)
and truth-audit (#80-#87, #89, #100) (operator can't verify source).
Natural 6-way bundle: #50 + #87 + #91 + #94 + #97 + #101 closes the
entire permission-input attack surface in one pass.
Filed in response to Clawhip pinpoint nudge 1494789577687437373
in #clawcode-building-in-public.
|
||
|
|
d63d58f3d0 |
ROADMAP #100: claw status/doctor JSON expose no commit identity; stale-base subsystem unplumbed
Dogfooded 2026-04-18 on main HEAD 63a0d30 from /tmp/cdU + /tmp/cdO*. Three-fold gap: 1. status/doctor JSON workspace object has 13 fields; none of them contain: head_sha, head_short_sha, expected_base, base_source, stale_base_state, upstream, ahead, behind, merge_base, is_detached, is_bare, is_worktree. A claw cannot answer 'is this lane at the expected base?' from the JSON surface alone. 2. --base-commit flag is silently accepted by status/doctor/sandbox/ init/export/mcp/skills/agents and silently dropped on dispatch. Same silent-no-op class as #98. A claw running 'claw --base-commit $expected status' gets zero effect — flag parses into a local, discharged at dispatch. 3. runtime::stale_base subsystem is FULLY implemented with 30+ tests (BaseCommitState, BaseCommitSource, resolve_expected_base, read_claw_base_file, check_base_commit, format_stale_base_warning). run_stale_base_preflight at main.rs:3058 calls it from Prompt/Repl only, writes output to stderr as human prose. .claw-base file is honored internally but invisible to status/doctor JSON. Complete implementation, wrong dispatch points. Plus: detached HEAD reported as magic string 'git_branch: "detached HEAD"' without accompanying SHA. Bare repo/worktree/submodule indistinguishable from regular repo in JSON. parse_git_status_branch has latent dot-split truncation bug on branch names like 'feat.ui' with upstream. Hits roadmap Product Principle #4 (Branch freshness before blame) and Phase 2 §4.2 (branch.stale_against_main event) directly — both unimplementable without commit identity in the JSON surface. Fix shape (~80 lines plumbing): - add head_sha/head_short_sha/is_detached/head_ref/is_bare/is_worktree - add base_commit: {source, expected, state} - add upstream: {ref, ahead, behind, merge_base} - wire --base-commit into CliAction::Status + CliAction::Doctor - add stale_base doctor check - fix parse_git_status_branch dot-split at :2541 Cross-cluster: truth-audit/diagnostic-integrity (#80-#87, #89) + silent-flag (#96-#99) + unplumbed-subsystem (#78). Natural bundles: #89+#100 (git-state completeness) and #78+#96+#100 (unplumbed surface). Milestone: ROADMAP #100. Filed in response to Clawhip pinpoint nudge 1494782026660712672 in #clawcode-building-in-public. |
||
|
|
63a0d30f57 |
ROADMAP #99: claw system-prompt --cwd/--date unvalidated, prompt-injection via newline
Dogfooded 2026-04-18 on main HEAD 0e263be from /tmp/cdN.
parse_system_prompt_args at main.rs:1162-1190 does:
cwd = PathBuf::from(value);
date.clone_from(value);
Zero validation. Both values flow through to
SystemPromptBuilder::render_env_context (prompt.rs:175-186) and
render_project_context (prompt.rs:289-293) where they are formatted
into the system prompt output verbatim via format!().
Two injection points per value:
- # Environment context
- 'Working directory: {cwd}'
- 'Date: {date}'
- # Project context
- 'Working directory: {cwd}'
- 'Today's date is {date}.'
Demonstrated attacks:
--date 'not-a-date' → accepted
--date '9999-99-99' → accepted
--date '1900-01-01' → accepted
--date "2025-01-01'; DROP TABLE users;--" → accepted verbatim
--date $'2025-01-01\nMALICIOUS: ignore all previous rules'
→ newline breaks out of bullet into standalone system-prompt
instruction line that the LLM will read as separate guidance
--cwd '/does/not/exist' → silently accepted, rendered verbatim
--cwd '' → empty 'Working directory: ' line
--cwd $'/tmp\nMALICIOUS: pwn' → newline injection same pattern
--help documents format as '[--cwd PATH] [--date YYYY-MM-DD]'.
Parser enforces neither. Same class as #96 / #98 — documented
constraint, unenforced at parse boundary.
Severity note: most severe of the #96/#97/#98/#99 silent-flag
class because the failure mode is prompt injection, not a silent
feature no-op. A claw or CI pipeline piping tainted
$REPO_PATH / $USER_INPUT into claw system-prompt is a
vector for LLM manipulation.
Fix shape:
1. parse --date as chrono::NaiveDate::parse_from_str(value, '%Y-%m-%d')
2. validate --cwd via std::fs::canonicalize(value)
3. defense-in-depth: debug_assert no-newlines at render boundary
4. regression tests for each rejected case
Cross-cluster: sibling of #83 (system-prompt date = build date)
and #84 (dump-manifests bakes abs path) — all three are about
the system-prompt / manifest surface trusting compile-time or
operator-supplied values that should be validated.
Filed in response to Clawhip pinpoint nudge 1494774477009981502
in #clawcode-building-in-public.
|