From d1608aede498eb54b2a87925bdde88e0e92a5c11 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 18 Apr 2026 17:03:14 +0900 Subject: [PATCH] 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ROADMAP.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index dd75e19..96c6ff7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4084,3 +4084,122 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label], **Blocker.** Policy decisions (strict vs JSON5; alias table meanings; fallback mode when config drop happens) overlap with #86 + #87 + #115 + #116 decisions. Resolving all five together as a "permission-posture-plus-config-parsing audit" would be efficient. **Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdVV` on main HEAD `7859222` in response to Clawhip pinpoint nudge at `1494955670791913508`. Extends #86 (silent-drop) with novel JSON5-partial-acceptance angle + alias-collapse security inversion. Joins **Permission-audit / tool-allow-list** (#94, #97, #101, #106, #115) as 6th member — this is the CONFIG-PARSE anchor of the permission-posture problem, completing the matrix: #87 absence (no config), #101 env-var fail-OPEN, #115 init-generated dangerous default, **#120** config-drops-to-dangerous-default. Joins **Truth-audit / diagnostic-integrity** on the `loaded_config_files=0` + `permission_mode=danger-full-access` inconsistency. Joins **Reporting-surface / config-hygiene** (#90, #91, #92, #110, #115, #116) on the silent-drop-plus-no-stderr-plus-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). Natural bundle: **#86 + #120** — config-parse reliability pair: silent-drop general case (#86) + JSON5-partial-acceptance + alias-inversion security flip (#120). Also **permission-drift-at-every-boundary 4-way**: #87 + #101 + #115 + **#120** — absence + env-var + init-generated + config-drop. Complete coverage of how a workspace can end up at `DangerFullAccess`. Also **Jobdori+gaebal-gajae mega-bundle** ("security-critical permission drift audit"): #86 + #87 + #101 + #115 + #116 + **#120** (five-way sweep of every path to wrong permissions). Session tally: ROADMAP #120. + +121. **`hooks` configuration schema is INCOMPATIBLE with Claude Code. claw-code expects `{"hooks": {"PreToolUse": [, ...]}}` — a flat array of command strings. Claude Code's schema is `{"hooks": {"PreToolUse": [{"matcher": "", "hooks": [{"type": "command", "command": "..."}]}]}}` — a matcher-keyed array of objects with nested command arrays. A user migrating their Claude Code `.claude.json` hooks block gets parse-fail: `field "hooks.PreToolUse" must be an array of strings, got an array (line 3)`. The error message is ALSO wrong — both schemas use arrays; the correct diagnosis is "array-of-objects where array-of-strings was expected." Separately, `claw --output-format json doctor` when failures present emits TWO concatenated JSON objects on stdout (`{kind:"doctor",...}` then `{type:"error",error:"doctor found failing checks"}`), breaking single-document parsing for any claw that does `json.load(stdout)`. Doctor output also has both `message` and `report` top-level fields containing identical prose — byte-duplicated** — dogfooded 2026-04-18 on main HEAD `b81e642` from `/tmp/cdWW`. + + **Concrete repro.** + ``` + # Claude Code hooks format: + $ cat > .claw/settings.json << 'EOF' + { + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + {"type": "command", "command": "echo PreToolUse-test >&2"} + ] + } + ] + } + } + EOF + + $ claw --output-format json status 2>&1 | head + {"error":"runtime config failed to load: /private/tmp/cdWW/.claw/settings.json: field \"hooks.PreToolUse\" must be an array of strings, got an array (line 3)","type":"error"} + # Error message: "must be an array of strings, got an array" — both are arrays. + # Correct diagnosis: "got an array of objects where an array of strings was expected." + + # claw-code's own expected format (flat string array): + $ cat > .claw/settings.json << 'EOF' + {"hooks": {"PreToolUse": ["echo hook-invoked >&2"]}} + EOF + $ claw --output-format json status | jq .permission_mode + "danger-full-access" + # Accepted. But this is not Claude Code format. + + # Claude Code canonical hooks: + # From Claude Code docs: + # { + # "hooks": { + # "PreToolUse": [ + # { + # "matcher": "Bash|Write|Edit", + # "hooks": [{"type": "command", "command": "./log-tool.sh"}] + # } + # ] + # } + # } + # None of the Claude Code hook features (matcher regex, typed commands, + # PostToolUse/Notification/Stop event types) are supported. + + # Separately: doctor NDJSON output on failures: + $ claw --output-format json doctor 2>&1 | python3 -c " + import json,sys; text=sys.stdin.read(); decoder=json.JSONDecoder() + idx=0; count=0 + while idx=len(text): break + obj,end=decoder.raw_decode(text,idx); count+=1 + print(f'Object {count}: keys={list(obj.keys())[:5]}') + idx=end + " + Object 1: keys=['checks', 'has_failures', 'kind', 'message', 'report'] + Object 2: keys=['error', 'type'] + # Two concatenated JSON objects on stdout. python json.load() fails with + # "Extra data: line 133 column 1". + + # Doctor message + report duplication: + $ claw --output-format json doctor 2>&1 | jq '.message == .report' + true + # Byte-identical prose in two top-level fields. + ``` + + **Trace path.** + - `rust/crates/runtime/src/config.rs:750-771` — `parse_optional_hooks_config`: + ```rust + fn parse_optional_hooks_config_object(...) -> Result { + let Some(hooks_value) = object.get("hooks") else { return Ok(...); }; + let hooks = expect_object(hooks_value, context)?; + Ok(RuntimeHookConfig { + pre_tool_use: optional_string_array(hooks, "PreToolUse", context)?.unwrap_or_default(), + post_tool_use: optional_string_array(hooks, "PostToolUse", context)?.unwrap_or_default(), + post_tool_use_failure: optional_string_array(hooks, "PostToolUseFailure", context)? + .unwrap_or_default(), + }) + } + ``` + `optional_string_array` expects `["cmd1", "cmd2"]`. Claude Code gives `[{"matcher": "...", "hooks": [{...}]}]`. Schema incompatible. + - `rust/crates/runtime/src/config.rs:775-779` — `validate_optional_hooks_config` calls the same parser; the error message "must be an array of strings" comes from `optional_string_array`'s path — but the user's actual input WAS an array (of objects). The message is technically correct but misleading. + - Claude Code hooks doc: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`, `SessionStart`. claw-code supports 3 event types. 5+ event types missing. + - `matcher` regex per hook (e.g. `"Bash|Write|Edit"`) — not supported. + - `type: "command"` vs `type: "http"` etc. (Claude Code extensibility) — not supported. + - `rust/crates/rusty-claude-cli/src/main.rs` doctor path — builds `DoctorReport` struct, renders BOTH a prose report AND emits it in `message` + `report` JSON fields. When failures present, appends a second `{"type":"error","error":"doctor found failing checks"}` to stdout. + + **Why this is specifically a clawability gap.** + 1. *Claude Code migration parity hard-block.* Users with existing `.claude.json` hooks cannot copy them over. Error message misleads them about what's wrong. No migration tool or adapter. + 2. *Feature gap: no matchers, no event types beyond 3.* PreToolUse/PostToolUse/PostToolUseFailure only. Missing Notification, UserPromptSubmit, Stop, SubagentStop, PreCompact, SessionStart — all of which are documented Claude Code capabilities claws rely on. + 3. *Error message lies about what's wrong.* "Must be an array of strings, got an array" — both are arrays. The correct message would be "expected an array of command strings, got an array of objects (Claude Code hooks format is not supported; see migration docs)." + 4. *Doctor NDJSON output breaks JSON consumers.* `--output-format json` promises a single JSON document per the flag name. Getting NDJSON (or rather: concatenated JSON objects without line separators) breaks every `json.load(stdout)` style consumer. + 5. *Byte-duplicated prose in `message` + `report`.* Two top-level fields with identical content. Parser ambiguity (which is the canonical source?). Byte waste. + 6. *Joins Claude Code migration parity* (#103, #109, #116, #117, #119, #120) as 7th member — hooks is the most load-bearing Claude Code feature that doesn't work. Users who rely on hooks for workflow automation (log-tool-calls.sh, format-on-edit.sh, require-bash-approval.sh) cannot migrate. + 7. *Joins truth-audit* — the diagnostic surface lies with a misleading error message. + 8. *Joins silent-flag / documented-but-unenforced* — `--output-format json` says "json" not "ndjson"; violation of the flag's own semantics. + + **Fix shape — extend the hooks schema to accept Claude Code format.** + 1. *Dual-schema hooks parser.* Accept either form: + - claw-code native: `["cmd1", "cmd2"]` + - Claude Code: `[{"matcher": "pattern", "hooks": [{"type": "command", "command": "..."}]}]` + Translate both to the internal `RuntimeHookConfig` representation. ~80 lines. + 2. *Add the missing event types.* Extend `RuntimeHookConfig` to include `UserPromptSubmit`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`, `SessionStart`. ~50 lines. + 3. *Implement matcher regex.* When a Claude Code-format hook includes `"matcher": "Bash|Write"`, apply the regex against the tool name before firing the hook. ~30 lines. + 4. *Fix the error message.* Change "must be an array of strings" to "expected an array of command strings. Claude Code hooks format (matcher + typed commands) is not yet supported — see ROADMAP #121 for migration path." ~10 lines. + 5. *Fix doctor NDJSON output.* Emit a single JSON object with `has_failures: true` + `error: "..."` fields rather than concatenating a separate error object. ~15 lines. + 6. *De-duplicate `message` and `report`.* Pick one (`report` is more descriptive for a doctor JSON surface); drop `message`. ~5 lines. + 7. *Regression tests.* (a) Claude Code hooks format parses and runs. (b) Native-format hooks still work. (c) Matcher regex matches correct tools. (d) All 8 event types dispatch. (e) Doctor failure emits single JSON object. (f) Doctor JSON has no duplicated fields. + + **Acceptance.** A user's `.claude.json` hooks block works verbatim as `.claw.json` hooks. Error messages correctly distinguish "wrong type for array elements" from "wrong element structure." `claw --output-format json doctor` emits exactly ONE JSON document regardless of failure state. No duplicated fields. + + **Blocker.** Implementation work is sizable (~200 lines + tests + migration docs). Product decision needed: full Claude Code hooks compatibility as a goal, or subset-plus-adapter. The current schema is claw-code-native; Claude Code compat requires either extending or replacing. + + **Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdWW` on main HEAD `b81e642` in response to Clawhip pinpoint nudge at `1494963222157983774`. Joins **Claude Code migration parity** (#103, #109, #116, #117, #119, #120) as 7th member — the most severe parity break since hooks is load-bearing automation infrastructure. Joins **Truth-audit / diagnostic-integrity** on misleading error message axis. Joins **Silent-flag / documented-but-unenforced** on NDJSON-output-violating-json-flag. Cross-cluster with **Unplumbed-subsystem** (#78, #96, #100, #102, #103, #107, #109, #111, #113) — hooks subsystem exists but schema is incompatible with the reference implementation. Natural bundle: **Claude Code migration parity septet (grown)**: #103 + #109 + #116 + #117 + #119 + #120 + **#121**. Complete coverage of every migration failure mode: silent drop (#103) + stderr prose warnings (#109) + hard-fail on unknown keys (#116) + prompt corruption from muscle memory (#117) + slash-verb fallthrough (#119) + JSON5-partial-accept + alias-inversion (#120) + hooks-schema-incompatible (#121). Also **#107 + #121** — hooks-subsystem pair: #107 hooks invisible to JSON diagnostics + #121 hooks schema incompatible with migration source. Also **NDJSON-violates-json-flag 2-way (new)**: #121 + probably more; worth sweep. Session tally: ROADMAP #121.