187 Commits

Author SHA1 Message Date
YeonGyu-Kim
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 2026-04-21 13:27:10 +09:00
YeonGyu-Kim
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 2026-04-21 12:27:06 +09:00
YeonGyu-Kim
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 2026-04-21 06:55:09 +09:00
YeonGyu-Kim
2c42f8bcc8 docs: remove duplicate ROADMAP #134 entry 2026-04-21 04:50:43 +09:00
YeonGyu-Kim
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 2026-04-21 01:55:42 +09:00
YeonGyu-Kim
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)
2026-04-20 15:30:34 +09:00
YeonGyu-Kim
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.
2026-04-20 15:06:50 +09:00
YeonGyu-Kim
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.
2026-04-20 15:04:08 +09:00
YeonGyu-Kim
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
2026-04-20 14:35:07 +09:00
YeonGyu-Kim
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) 2026-04-20 13:11:12 +09:00
YeonGyu-Kim
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 2026-04-20 13:03:50 +09:00
YeonGyu-Kim
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 2026-04-20 12:52:22 +09:00
YeonGyu-Kim
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 2026-04-20 12:43:11 +09:00
YeonGyu-Kim
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 2026-04-20 12:32:56 +09:00
YeonGyu-Kim
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 2026-04-20 12:05:05 +09:00
YeonGyu-Kim
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.
2026-04-18 20:32:52 +09:00
YeonGyu-Kim
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.
2026-04-18 20:03:32 +09:00
YeonGyu-Kim
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.
2026-04-18 19:03:02 +09:00
YeonGyu-Kim
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.
2026-04-18 18:38:24 +09:00
YeonGyu-Kim
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.
2026-04-18 18:03:35 +09:00
YeonGyu-Kim
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.
2026-04-18 17:03:14 +09:00
YeonGyu-Kim
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.
2026-04-18 16:34:19 +09:00
YeonGyu-Kim
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.
2026-04-18 16:03:37 +09:00
YeonGyu-Kim
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.
2026-04-18 15:32:30 +09:00
YeonGyu-Kim
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.
2026-04-18 15:01:47 +09:00
YeonGyu-Kim
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.
2026-04-18 14:03:20 +09:00
YeonGyu-Kim
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.
2026-04-18 13:32:46 +09:00
YeonGyu-Kim
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.
2026-04-18 12:09:31 +09:00
YeonGyu-Kim
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.
2026-04-18 11:33:10 +09:00
YeonGyu-Kim
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.
2026-04-18 11:03:12 +09:00
YeonGyu-Kim
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.
2026-04-18 10:34:25 +09:00
YeonGyu-Kim
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.
2026-04-18 10:05:31 +09:00
YeonGyu-Kim
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.
2026-04-18 09:34:05 +09:00
YeonGyu-Kim
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.
2026-04-18 09:05:32 +09:00
YeonGyu-Kim
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.
2026-04-18 08:05:20 +09:00
YeonGyu-Kim
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.
2026-04-18 07:33:47 +09:00
YeonGyu-Kim
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.
2026-04-18 07:08:25 +09:00
YeonGyu-Kim
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.
2026-04-18 06:34:38 +09:00
YeonGyu-Kim
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.
2026-04-18 06:03:22 +09:00
YeonGyu-Kim
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.
2026-04-18 05:34:30 +09:00
YeonGyu-Kim
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.
2026-04-18 05:04:28 +09:00
YeonGyu-Kim
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.
2026-04-18 04:36:47 +09:00
YeonGyu-Kim
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.
2026-04-18 04:03:29 +09:00
YeonGyu-Kim
0e263bee42 ROADMAP #98: --compact silently ignored in 9 dispatch paths + stdin-piped Prompt hardcodes compact=false
Dogfooded 2026-04-18 on main HEAD 7a172a2 from /tmp/cdM.

--help at main.rs:8251 documents --compact as 'text mode only;
useful for piping.' The implementation knows the constraint but
never enforces it at the parse boundary — the flag is silently
dropped in every non-{Prompt+Text} dispatch path:

1. --output-format json prompt: run_turn_with_output (:3807-3817)
   has no CliOutputFormat::Json if compact arm; JSON branch
   ignores compact entirely
2. status/sandbox/doctor/init/export/mcp/skills/agents: those
   CliAction variants have no compact field at all; parse_args
   parses --compact into a local bool and then discharges it
   with nowhere to go on dispatch
3. claw --compact with piped stdin: the stdin fallthrough at
   main.rs:614 hardcodes compact: false regardless of the
   user-supplied --compact — actively overriding operator intent

No error, no warning, no diagnostic. A claw using
claw --compact --output-format json '...' to pipe-friendly output
gets full verbose JSON silently.

Fix shape:
- reject --compact + --output-format json at parse time (~5 lines)
- reject --compact on non-Prompt subcommands with a named error
  (~15 lines)
- honor --compact in stdin-piped Prompt fallthrough: change
  compact: false to compact at :614 (1 line)
- optionally add CliOutputFormat::Json if compact arm if
  compact-JSON is desirable

Joins silent-flag no-op class with #96 (Resume-safe leak) and
#97 (silent-empty allow-set). Natural bundle #96+#97+#98 covers
the --help/flag-validation hygiene triangle.

Filed in response to Clawhip pinpoint nudge 1494766926826700921
in #clawcode-building-in-public.
2026-04-18 03:32:57 +09:00
YeonGyu-Kim
7a172a2534 ROADMAP #97: --allowedTools empty-string silently blocks all tools, no observable signal
Dogfooded 2026-04-18 on main HEAD 3ab920a from /tmp/cdL.

Silent vs loud asymmetry for equivalent mis-input at the
tool-allow-list knob:
- `--allowedTools "nonsense"` → loud structured error naming
  every valid tool (works as intended)
- `--allowedTools ""` (shell-expansion failure, $TOOLS expanded
  empty) → silent Ok(Some(BTreeSet::new())) → all tools blocked
- `--allowedTools ",,"` → same silent empty set
- `.claw.json` with `allowedTools` → fails config load with
  'unknown key allowedTools' — config-file surface locked out,
  CLI flag is the only knob, and the CLI flag has the footgun

Trace: tools/src/lib.rs:192-248 normalize_allowed_tools. Input
values=[""] is NOT empty (len=1) so the early None guard at
main.rs:1048 skips. Inner split/filter on empty-only tokens
produces zero elements; the error-producing branch never runs.
Returns Ok(Some(empty)), which downstream filter treats as
'allow zero tools' instead of 'allow all tools.'

No observable recovery: status JSON exposes kind/model/
permission_mode/sandbox/usage/workspace but no allowed_tools
field. doctor check set has no tool_restrictions category. A
lane that silently restricted itself to zero tools gets no
signal until an actual tool call fails at runtime.

Fix shape: reject empty-token input at parse time with a clear
error. Add explicit --allowedTools none opt-in if zero-tool
lanes are desirable. Surface active allow-set in status JSON
and as a doctor check. Consider supporting allowedTools in
.claw.json or improving its rejection message.

Joins permission-audit sweep (#50/#87/#91/#94) on the
tool-allow-list axis. Sibling of #86 on the truth-audit side:
both are 'misconfigured claws have no observable signal.'

Filed in response to Clawhip pinpoint nudge 1494759381068419115
in #clawcode-building-in-public.
2026-04-18 03:04:08 +09:00
YeonGyu-Kim
3ab920ac30 ROADMAP #96: claw --help Resume-safe summary leaks 62 STUB_COMMANDS entries
Dogfooded 2026-04-18 on main HEAD 8db8e49 from /tmp/cdK. Partial
regression of ROADMAP #39 / #54 at the help-output layer.

'claw --help' emits two separate slash-command enumerations:
(1) Interactive slash commands block -- correctly filtered via
    render_slash_command_help_filtered(STUB_COMMANDS) at main.rs:8268
(2) Resume-safe commands one-liner -- UNFILTERED, emits every entry
    from resume_supported_slash_commands() at main.rs:8270-8278

Programmatic cross-check: intersect the Resume-safe listing with
STUB_COMMANDS (60+ entries at main.rs:7240-7320) returns 62
overlaps: budget, rate-limit, metrics, diagnostics, workspace,
reasoning, changelog, bookmarks, allowed-tools, tool-details,
language, max-tokens, temperature, system-prompt, output-style,
privacy-settings, keybindings, thinkback, insights, stickers,
advisor, brief, summary, vim, and more. All advertised as
resume-safe; all produce 'Did you mean /X' stub-guard errors when
actually invoked in resume mode.

Fix shape: one-line filter at main.rs:8270 adding
.filter(|spec| !STUB_COMMANDS.contains(&spec.name)) or extract
shared helper resume_supported_slash_commands_filtered. Add
regression test parallel to stub_commands_absent_from_repl_
completions that parses the Resume-safe line and asserts no entry
matches STUB_COMMANDS.

Filed in response to Clawhip pinpoint nudge 1494751832399024178 in
#clawcode-building-in-public.
2026-04-18 02:35:06 +09:00
YeonGyu-Kim
8db8e4902b ROADMAP #95: skills install is user-scope only, no uninstall, leaks across workspaces
Dogfooded 2026-04-18 on main HEAD b7539e6 from /tmp/cdJ. Three
stacked gaps on the skill-install surface:

(1) User-scope only install. default_skill_install_root at
    commands/src/lib.rs returns CLAW_CONFIG_HOME/skills ->
    CODEX_HOME/skills -> HOME/.claw/skills -- all user-level. No
    project-scope code path. Installing from workspace A writes to
    ~/.claw/skills/X and makes X active:true in every other
    workspace with source.id=user_claw.

(2) No uninstall. claw --help enumerates /skills
    [list|install|help|<skill>] -- no uninstall. 'claw skills
    uninstall X' falls through to prompt-dispatch. REPL /skill is
    identical. Removing a bad skill requires manual rm -rf on the
    installed path parsed out of install receipt output.

(3) No scope signal. Install receipt shows 'Registry
    /Users/yeongyu/.claw/skills' but the operator is never asked
    project vs user, and JSON receipt does not distinguish install
    scope.

Doubly compounds with #85 (skill discovery ancestor walk): an
attacker who can write under an ancestor OR can trick the operator
into one bad 'skills install' lands a skill in the user-level
registry that's active in every future claw invocation.

Runs contrary to the project/user/local three-tier scope settings
already use (User / Project / Local via ConfigSource). Skills
collapse all three onto User at install time.

Fix shape (~60 lines): --scope user|project|local flag on skills
install (no default in --output-format json mode, prompt
interactively); claw skills uninstall + /skills uninstall
slash-command; installed_path per skill record in --output-format
json skills output.

Filed in response to Clawhip pinpoint nudge 1494744278423961742 in
#clawcode-building-in-public.
2026-04-18 02:03:10 +09:00
YeonGyu-Kim
b7539e679e ROADMAP #94: permission rules accept typos, case-sensitive match disagrees with ecosystem convention, invisible in all diagnostic surfaces
Dogfooded 2026-04-18 on main HEAD 7f76e6b from /tmp/cdI. Three
stacked failures on the permission-rule surface:

(1) Typo tolerance. parse_optional_permission_rules at
    runtime/src/config.rs:780-798 is just optional_string_array with
    no per-entry validation. Typo rules like 'Reed', 'Bsh(echo:*)',
    'WebFech' load silently; doctor reports config: ok.

(2) Case-sensitive match against lowercase runtime names.
    PermissionRule::matches does self.tool_name != tool_name strict
    compare. Runtime registers tools lowercase (bash).
    Claude Code convention / MCP docs use capitalized (Bash). So
    'deny: ["Bash(rm:*)"]' never fires because tool_name='bash' !=
    rule.tool_name='Bash'. Cross-harness config portability fails
    open, not closed.

(3) Loaded rules invisible. status JSON has no permission_rules
    field. doctor has no rules check. A clawhip preflight asking
    'does this lane actually deny Bash(rm:*)?' has no
    machine-readable answer; has to re-parse .claw.json and
    re-implement parse semantics.

Contrast: --allowedTools CLI flag HAS tool-name validation with a
50+ tool registry. The same registry is not consulted when parsing
permissions.allow/deny/ask. Asymmetric validation, same shape as
#91 (config accepts more permission-mode labels than CLI).

Fix shape (~30-45 lines): validate rule tool names against the
same registry --allowedTools uses; case-fold tool_name compare in
PermissionRule::matches; expose loaded rules in status/doctor JSON
with unknown_tool flag.

Filed in response to Clawhip pinpoint nudge 1494736729582862446 in
#clawcode-building-in-public.
2026-04-18 01:34:15 +09:00
YeonGyu-Kim
7f76e6bbd6 ROADMAP #93: --resume reference heuristic forks silently; no workspace scoping
Dogfooded 2026-04-18 on main HEAD bab66bb from /tmp/cdH.
SessionStore::resolve_reference at runtime/src/session_control.rs:
86-116 branches on a textual heuristic -- looks_like_path =
direct.extension().is_some() || direct.components().count() > 1.
Same-looking reference triggers two different code paths:

Repros:
- 'claw --resume session-123' -> managed store lookup (no extension,
  no slash) -> 'session not found: session-123'
- 'claw --resume session-123.jsonl' -> workspace-relative file path
  (extension triggers path branch) -> opens /cwd/session-123.jsonl,
  succeeds if present
- 'claw --resume /etc/passwd' -> absolute path opened verbatim,
  fails only because JSONL parse errors ('invalid JSONL record at
  line 1: unexpected character: #')
- 'claw --resume /etc/hosts' -> same; file is read, structural
  details (first char, line number) leak in error
- symlink inside .claw/sessions/<fp>/passwd-symlink.jsonl pointing
  at /etc/passwd -> claw --resume passwd-symlink follows it

Clawability impact: operators copying session ids from /session
list naturally try adding .jsonl and silently hit the wrong branch.
Orchestrators round-tripping session ids through --resume cannot
do any path normalization without flipping lookup modes. No
workspace scoping, so any readable file on disk is a valid target.
Symlinks inside managed path escape the workspace silently.

Fix shape (~15 lines minimum): canonicalize the resolved candidate
and assert prefix match with workspace_root before opening; return
OutsideWorkspace typed error otherwise. Optional cleanup: split
--resume <id> and --resume-file <path> into explicit shapes.

Filed in response to Clawhip pinpoint nudge 1494729188895359097 in
#clawcode-building-in-public.
2026-04-18 01:04:37 +09:00
YeonGyu-Kim
bab66bb226 ROADMAP #92: MCP config does not expand ${VAR} or ~/ — standard configs fail silently
Dogfooded 2026-04-18 on main HEAD d0de86e from /tmp/cdE. MCP
command, args, url, headers, headersHelper config fields are
loaded and passed to execve/URL-parse verbatim. No ${VAR}
interpolation, no ~/ home expansion, no preflight check, no doctor
warning.

Repros:
- {'command':'~/bin/my-server','args':['~/config/file.json']} ->
  execve('~/bin/my-server', ['~/config/file.json']) -> ENOENT at
  MCP connect time.
- {'command':'${HOME}/bin/my-server','args':['--tenant=${TENANT_ID}']}
  -> literal ${HOME}/bin/my-server handed to execve; literal
  ${TENANT_ID} passed to the server as tenant argument.
- {'headers':{'Authorization':'Bearer ${API_TOKEN}'}} -> literal
  string 'Bearer ${API_TOKEN}' sent as HTTP header.

Trace: parse_mcp_server_config in runtime/src/config.rs stores
strings raw; McpStdioProcess::spawn at mcp_stdio.rs:1150-1170 is
Command::new(&transport.command).args(&transport.args).spawn().
grep interpolate/expand_env/substitute/${ across runtime/src/
returns empty outside format-string literals.

Clawability impact: every public MCP server README uses ${VAR}/~/
in examples; copy-pasted configs load with doctor:ok and fail
opaquely at spawn with generic ENOENT that has lost the context
about why. Operators forced to hardcode secrets in .claw.json
(triggering #90) or wrap commands in shell scripts -- both worse
security postures than the ecosystem norm. Cross-harness round-trip
from Claude Code /.mcp.json breaks when interpolation is present.

Fix shape (~50 lines): config-load-time interpolation of ${VAR}
and leading ~/ in command/args/url/headers/headers_helper; missing-
variable warnings captured into ConfigLoader all_warnings; optional
{'config':{'expand_env':false}} toggle; mcp_config_interpolation
doctor check that flags literal ${ / ~/ remaining after substitution.

Filed in response to Clawhip pinpoint nudge 1494721628917989417 in
#clawcode-building-in-public.
2026-04-18 00:35:44 +09:00