Skip to content

Commit f2d6538

Browse files
committed
ROADMAP ultraworkers#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 (ultraworkers#103, ultraworkers#109) as 3rd member — most severe migration break. ultraworkers#103 silently drops .md files, ultraworkers#109 stderr-prose warnings, ultraworkers#116 outright hard-fails. Joins Reporting-surface/config-hygiene (ultraworkers#90, ultraworkers#91, ultraworkers#92, ultraworkers#110, ultraworkers#115) on error-routing-vs-stdout axis. Joins Silent-flag/documented-but-unenforced (ultraworkers#96-ultraworkers#101, ultraworkers#104, ultraworkers#108, ultraworkers#111, ultraworkers#115) — only first error reported, rest silent. Cross-cluster with Truth-audit — validation.is_ok() hides all but first structured problem. Natural bundles: ultraworkers#103 + ultraworkers#109 + ultraworkers#116 — Claude Code migration parity triangle: loss of compat (.md dropped) + loss of structure (stderr prose warnings) + loss of forward-compat (unknowns hard-fail) ultraworkers#109 + ultraworkers#116 — config validation reporting surface: only first warning surfaces structurally (ultraworkers#109) only first error surfaces structurally AND halts (ultraworkers#116) Filed in response to Clawhip pinpoint nudge 1494925472239321160 in #clawcode-building-in-public.
1 parent ad02761 commit f2d6538

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

ROADMAP.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3609,3 +3609,98 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
36093609
**Blocker.** Product decision: is `init`-default `danger-full-access` intentional (for low-friction onboarding) or accidental? If intentional, the fix is warning-only. If accidental, the fix is a safer default.
36103610
36113611
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdPP` on main HEAD `ca09b6b` in response to Clawhip pinpoint nudge at `1494917922076889139`. Joins **Permission-audit / tool-allow-list** (#94, #97, #101, #106) as 5th member — this is the init-time ANCHOR of the permission-posture problem: #87 is absence-of-config, #101 is fail-OPEN on bad env var, **#115** is the init-generated dangerous default. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111) on the third axis: not a silent flag, but a silent setting (the generated config's security implications are silent in the init output). Cross-cluster with **Reporting-surface / config-hygiene** (#90, #91, #92, #110) on the structured-data-vs-prose axis: `claw init --output-format json` wraps all structure inside `message`. Cross-cluster with **Truth-audit** on "Next step: Review and tailor the generated guidance" phrasing — misleads by omission. Natural bundle: **#87 + #101 + #115** — "permission drift at every boundary": absence default + env-var bypass + init-generated default. Also: **#50 + #87 + #91 + #94 + #97 + #101 + #115** — flagship permission-audit sweep now 7-way. Session tally: ROADMAP #115.
3612+
3613+
116. **Unknown keys in `.claw.json` are strict ERRORS, not warnings — `claw` hard-fails at startup with exit 1 if any field is unrecognized. Only the FIRST error is reported; all subsequent validation messages are lost. Valid Claude Code config fields (`apiKeyHelper`, `env`, and other Claude-Code-native keys) trigger the same hard-fail, so a user renaming `.claude.json → .claw.json` for migration gets `"unknown key \"apiKeyHelper\"" ... exit 1` with zero guidance on what to delete. The error goes to stderr as structured JSON (`{"type":"error","error":"..."}`) but a `--output-format json` consumer has to read BOTH stdout AND stderr to capture success-or-error — the stdout side is empty on error. There is no `--ignore-unknown-config` flag, no `strict` vs `warn` mode toggle, no forward-compat path — a claw's future-self putting a single new field in the config kills every older claw binary** — dogfooded 2026-04-18 on main HEAD `ad02761` from `/tmp/cdRR`.
3614+
3615+
**Concrete repro.**
3616+
```
3617+
# Forward-compat scenario — config has a "future" field:
3618+
$ cd /tmp/cdRR && git init -q .
3619+
$ cat > .claw.json << 'EOF'
3620+
{
3621+
"permissions": {"defaultMode": "default"},
3622+
"futureField": "some-feature"
3623+
}
3624+
EOF
3625+
$ claw --output-format json status
3626+
# stdout: (empty)
3627+
# stderr: {"type":"error","error":"/private/tmp/cdRR/.claw.json: unknown key \"futureField\" (line 3)"}
3628+
# exit: 1
3629+
3630+
# Claude Code migration scenario — rename .claude.json to .claw.json:
3631+
$ cat > .claw.json << 'EOF'
3632+
{
3633+
"permissions": {"defaultMode": "default"},
3634+
"apiKeyHelper": "/usr/local/bin/key-helper",
3635+
"env": {"FOO": "bar"}
3636+
}
3637+
EOF
3638+
$ claw --output-format json status
3639+
# stderr: {"type":"error","error":"/private/tmp/cdRR/.claw.json: unknown key \"apiKeyHelper\""}
3640+
# apiKeyHelper is a real Claude Code config field. claw-code refuses it.
3641+
3642+
# Multiple unknowns — only the first is reported:
3643+
$ cat > .claw.json << 'EOF'
3644+
{
3645+
"a_bad": 1,
3646+
"b_bad": 2,
3647+
"c_bad": 3
3648+
}
3649+
EOF
3650+
$ claw --output-format json status
3651+
# stderr: unknown key "a_bad" (line 2)
3652+
# User fixes a_bad, re-runs, gets b_bad error. Iterative discovery.
3653+
3654+
# No escape hatch:
3655+
$ claw --ignore-unknown-config --output-format json status
3656+
# stderr: unknown option: --ignore-unknown-config
3657+
```
3658+
3659+
**Trace path.**
3660+
- `rust/crates/runtime/src/config.rs:282-291` — `ConfigLoader` validation gate:
3661+
```rust
3662+
let validation = crate::config_validate::validate_config_file(
3663+
&parsed.object,
3664+
&parsed.source,
3665+
&entry.path,
3666+
);
3667+
if !validation.is_ok() {
3668+
let first_error = &validation.errors[0];
3669+
return Err(ConfigError::Parse(first_error.to_string()));
3670+
}
3671+
all_warnings.extend(validation.warnings);
3672+
```
3673+
`validation.is_ok()` means `errors.is_empty()`. Any error in the vec halts loading. Only `errors[0]` is surfaced. `validation.warnings` is accumulated and later `eprintln!`d as prose (already covered in #109).
3674+
- `rust/crates/runtime/src/config_validate.rs:19-47` — `DiagnosticKind::UnknownKey`:
3675+
```rust
3676+
UnknownKey { suggestion: Option<String> }
3677+
```
3678+
Unknown keys produce a `ConfigDiagnostic` with `level: DiagnosticLevel::Error`. They're classified as errors, not warnings.
3679+
- `rust/crates/runtime/src/config_validate.rs:380-395` — unknown-key detection walks the parsed object, compares keys against a hard-coded known list, emits `Error`-level diagnostics for any mismatch.
3680+
- `rust/crates/runtime/src/config_validate.rs` — `SCHEMA_FIELDS` or equivalent allow-list is a fixed set. There is no forward-compat extension mechanism (no `extensions` / `x-*` prefix convention, no reserved namespace, no `additionalProperties` toggle).
3681+
- `grep -rn "apiKeyHelper" rust/crates/runtime/` → zero matches. Claude-Code-native fields are not recognized even as no-ops; they are outright rejected.
3682+
- `grep -rn "ignore.*unknown\|--no-validate\|strict.*validation" rust/crates/` → zero matches. No escape hatch.
3683+
3684+
**Why this is specifically a clawability gap.**
3685+
1. *Forward-compat is impossible.* If a claw upgrade adds a new config field, any older binary (CI cache, legacy nodes, stuck deployments) hard-fails on the new field. This is the opposite of how tools like `cargo`, `jq`, most JSON APIs, and every serde-derived Rust config loader handle unknowns (warn or silently accept by default).
3686+
2. *Only `errors[0]` is reported per run.* Fixing N unknown fields requires N edit-run-fix cycles. A claw running `claw status` inside a validation loop has to re-invoke for every unknown. This joins #109 where only the first error surfaces structurally; the rest are discarded.
3687+
3. *Claude Code migration parity is broken.* The README and user docs for claw-code position it as Claude-Code-compatible. Users who literally `cp .claude.json .claw.json` get immediate hard-fail on `apiKeyHelper`, `env`, and other legitimate Claude Code fields. No graceful "this is a Claude Code field we don't support, ignored" message.
3688+
4. *Error-routing split.* With `--output-format json`, success goes to stdout, errors go to stderr. Claws orchestrating claw must capture both streams and correlate. A claw that `claw status | jq .permission_mode` silently gets empty output when config is broken — the error is invisible to the pipe consumer.
3689+
5. *Joins #109 (validation warnings stderr-only).* #109 said warnings are prose-on-stderr and the structured form is discarded. #116 adds: errors also go to stderr (structured as JSON this time, good), but in a hard-fail way that prevents the stdout channel from emitting ANYTHING. A claw gets either pure-JSON success or empty-stdout + JSON-error-stderr; it must always read both.
3690+
6. *No strict-vs-lax mode.* Tools that support forward-compat typically have two modes: strict (reject unknown) for production, lax (warn on unknown) for developer workflows. claw-code has neither toggle; it's strict always.
3691+
7. *Joins Claude Code migration parity cluster* (#103, #109). #103 was `claw agents` dropping non-`.toml` files. #109 was stderr-only prose warnings. **#116** is the outright rejection of Claude-Code-native config fields at load time.
3692+
3693+
**Fix shape — make unknown keys warnings by default, add explicit strict mode, collect all errors per run.**
3694+
1. *Downgrade `DiagnosticKind::UnknownKey` from Error to Warning by default.* The parser still surfaces the diagnostic; the CLI just doesn't halt on it. ~5 lines.
3695+
2. *Add `strict` mode flag.* `.claw.json` top-level `{"strictValidation": true}` OR `--strict-config` CLI flag. When set, unknown keys become errors as today. Default: off. ~15 lines.
3696+
3. *Collect all diagnostics, don't halt on first.* Replace `errors[0]` return with full `errors: [...]` collection, then decide fatal-or-not based on severity + strict-mode flag. ~20 lines.
3697+
4. *Recognize Claude-Code-native fields as explicit no-ops.* Add `apiKeyHelper`, `env`, and other known Claude Code fields to a `TOLERATED_CLAUDE_CODE_FIELDS` allow-list that emits a migration-hint warning: `"apiKeyHelper" is a Claude Code field not yet supported by claw-code; ignored.` ~30 lines.
3698+
5. *Include structured errors in the `--output-format json` stdout payload on hard fail.* Currently `{"type":"error","error":"..."}` goes to stderr and stdout is empty. Emit a machine-readable error envelope on stdout as well (or exclusively), with `config_diagnostics: [{level, field, location, message}]`. Keep stderr human-readable. ~15 lines.
3699+
6. *Add suggestion-by-default for UnknownKey.* The parser already supports `suggestion: Option<String>` in the DiagnosticKind — wire it to a fuzzy-match across the schema. `"permisions"` → `"permissions"` suggestion. ~15 lines.
3700+
7. *Regression tests.* (a) Forward-compat config with novel field loads without error. (b) Strict mode opt-in rejects unknown. (c) All diagnostics reported, not just first. (d) apiKeyHelper + env + other Claude Code fields produce migration-hint warning, not hard-fail. (e) `--output-format json` stdout contains error envelope on validation failure.
3701+
3702+
**Acceptance.** `cp .claude.json .claw.json && claw status` loads without hard-fail and emits a migration-hint warning for each Claude-Code-native field. `echo '{"newFutureField": 1}' > .claw.json && claw status` loads with a single warning, not a fatal error. `claw --strict-config status` retains today's strict behavior. All diagnostics are reported, not just `errors[0]`. `--output-format json` emits errors on stdout in addition to stderr.
3703+
3704+
**Blocker.** Policy decision: does the project want strict-by-default (current) or lax-by-default? The fix shape assumes lax-by-default with strict opt-in, matching industry-standard forward-compat conventions and easing Claude Code migration.
3705+
3706+
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdRR` on main HEAD `ad02761` in response to Clawhip pinpoint nudge at `1494925472239321160`. Joins **Claude Code migration parity** (#103, #109) as 3rd member — this is the most severe migration-parity break, since it's a HARD FAIL at startup rather than a silent drop (#103) or a stderr-prose warning (#109). Joins **Reporting-surface / config-hygiene** (#90, #91, #92, #110, #115) on the error-routing-vs-stdout axis: `--output-format json` consumers get empty stdout on config errors. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115) because only the first error is reported and all subsequent errors are silent. Cross-cluster with **Truth-audit / diagnostic-integrity** (#80–#87, #89, #100, #102, #103, #105, #107, #109, #110, #112, #114, #115) because `validation.is_ok()` hides all-but-the-first structured problem. Natural bundle: **#103 + #109 + #116** — Claude Code migration parity triangle: `claw agents` drops `.md` (loss of compatibility) + config warnings stderr-prose (loss of structure) + config unknowns hard-fail (loss of forward-compat). Also **#109 + #116** — config validation reporting surface: only first warning surfaces structurally (#109) + only first error surfaces structurally and halts loading (#116). Session tally: ROADMAP #116.

0 commit comments

Comments
 (0)