mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-25 13:44:06 +08:00
897 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3848ea64e3 |
ROADMAP #118: /stats, /tokens, /cache all collapse to SlashCommand::Stats; 3-way dispatch collapse with 3 distinct help descriptions
Dogfooded 2026-04-18 on main HEAD b9331ae from /tmp/cdTT.
Three slash commands collapse to one handler:
$ claw --help | grep -E '^\s*/(stats|tokens|cache)\s'
/stats Show workspace and session statistics [resume]
/tokens Show token count for the current conversation [resume]
/cache Show prompt cache statistics [resume]
Three distinct promises. One implementation:
$ claw --resume s --output-format json /stats
{"kind":"stats","input_tokens":0,"output_tokens":0,
"cache_creation_input_tokens":0,"cache_read_input_tokens":0,
"total_tokens":0}
$ claw --resume s --output-format json /tokens
{"kind":"stats", ...identical...}
$ claw --resume s --output-format json /cache
{"kind":"stats", ...identical...}
diff /stats /tokens → empty
diff /stats /cache → empty
kind field is always 'stats', never 'tokens' or 'cache'.
Trace:
commands/src/lib.rs:1405-1408:
'stats' | 'tokens' | 'cache' => {
validate_no_args(command, &args)?;
SlashCommand::Stats
}
commands/src/lib.rs:317 SlashCommandSpec name='stats' registered
commands/src/lib.rs:702 SlashCommandSpec name='tokens' registered
SlashCommandSpec name='cache' also registered
Each has distinct summary/description in help.
No SlashCommand::Tokens or SlashCommand::Cache variant exists.
main.rs:2872-2879 SlashCommand::Stats handler hard-codes
'kind': 'stats' regardless of which alias invoked.
More severe than #111:
#111: /providers → Doctor (2-way collapse, wildly wrong category)
#118: /stats + /tokens + /cache → Stats (3-way collapse with
THREE distinct advertised purposes)
The collapse hides information that IS available. /stats output
has cache_creation_input_tokens + cache_read_input_tokens as
top-level fields, so cache data is PRESENT. But /cache should
probably return {kind:'cache', cache_hits, cache_misses,
hit_rate}, a cache-specific schema. Similarly /tokens should
return {kind:'tokens', conversation_total, turns,
average_per_turn}. Implementation returns the union for all.
Fix shape (~90 lines):
- Add SlashCommand::Tokens and SlashCommand::Cache variants
- Parser arms:
'tokens' => SlashCommand::Tokens
'cache' => SlashCommand::Cache
'stats' => SlashCommand::Stats
- Handlers with distinct output schemas:
/tokens: {kind:'tokens', conversation_total, input_tokens,
output_tokens, turns, average_per_turn}
/cache: {kind:'cache', cache_creation_input_tokens,
cache_read_input_tokens, cache_hits, cache_misses,
hit_rate_pct}
/stats: {kind:'stats', subsystem:'all', ...}
- Regression per alias: kind matches, schema matches purpose
- Sweep parser for other collapse arms
- If aliasing intentional, annotate --help with (alias for X)
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116, #117) as 13th — more severe than #111.
Joins Truth-audit on help-vs-implementation mismatch axis.
Cross-cluster with Parallel-entry-point asymmetry on multiple-
surfaces-identical-implementation axis.
Natural bundles:
#111 + #118 — dispatch-collapse pair:
/providers → Doctor (2-way, wildly wrong)
/stats+/tokens+/cache → Stats (3-way, distinct purposes)
Complete parser-dispatch audit shape.
#108 + #111 + #118 — parser-level trust gaps:
typo fallthrough (#108) +
2-way collapse (#111) +
3-way collapse (#118)
Filed in response to Clawhip pinpoint nudge 1494940571385593958
in #clawcode-building-in-public.
|
||
|
|
b9331ae61b |
ROADMAP #117: -p flag is super-greedy, swallows all subsequent args into prompt; --help/--version/--model after -p silently consumed; flag-like prompts bypass emptiness check
Dogfooded 2026-04-18 on main HEAD f2d6538 from /tmp/cdSS.
-p (Claude Code compat shortcut) at main.rs:524-538:
"-p" => {
let prompt = args[index + 1..].join(" ");
if prompt.trim().is_empty() {
return Err(...);
}
return Ok(CliAction::Prompt {...});
}
args[index+1..].join(" ") = ABSORBS EVERY subsequent arg.
return Ok(...) = short-circuits parser, discards wants_help etc.
Failure modes:
1. Silent flag swallow:
claw -p "test" --model sonnet --output-format json
→ prompt = "test --model sonnet --output-format json"
→ model: default (not sonnet), format: text (not json)
→ LLM receives literal string '--model sonnet' as user input
→ billable tokens burned on corrupted prompt
2. --help/--version defeated:
claw -p "test" --help → sends 'test --help' to LLM
claw -p "test" --version → sends 'test --version' to LLM
claw --help -p "test" → wants_help=true set, then discarded
by -p's early return. Help never prints.
3. Emptiness check too weak:
claw -p --model sonnet
→ prompt = "--model sonnet" (non-empty)
→ passes is_empty() check
→ sends '--model sonnet' to LLM as the user prompt
→ no error raised
4. Flag-order invisible:
claw --model sonnet -p "test" → WORKS (model parsed first)
claw -p "test" --model sonnet → BROKEN (--model swallowed)
Same flags, different order, different behavior.
--help has zero warning about flag-order semantics.
Compare Claude Code:
claude -p "prompt" --model sonnet → works (model takes effect)
claw -p "prompt" --model sonnet → silently broken
Fix shape (~40 lines):
- "-p" takes exactly args[index+1] as prompt, continues parsing:
let prompt = args.get(index+1).cloned().unwrap_or_default();
if prompt.trim().is_empty() || prompt.starts_with('-') {
return Err("-p requires a prompt string");
}
pending_prompt = Some(prompt);
index += 2;
- Reject prompts that start with '-' unless preceded by '--':
'claw -p -- --literal-prompt' = literal '--literal-prompt'
- Consult wants_help before returning from -p branch.
- Regression tests:
-p "prompt" --model sonnet → model takes effect
-p "prompt" --help → help prints
-p --foo → error
--help -p "test" → help prints
-p -- --literal → literal prompt sent
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115, #116) as 12th — -p is undocumented in --help
yet actively broken.
Joins Parallel-entry-point asymmetry (#91, #101, #104, #105,
#108, #114) as 7th — three entry points (prompt TEXT, bare
positional, -p TEXT) with subtly different arg-parsing.
Joins Claude Code migration parity (#103, #109, #116) as 4th —
users typing 'claude -p "..." --model ...' muscle memory get
silent prompt corruption.
Joins Truth-audit — parser lies about what it parsed.
Natural bundles:
#108 + #117 — billable-token silent-burn pair:
typo fallthrough burns tokens (#108) +
flag-swallow burns tokens (#117)
#105 + #108 + #117 — model-resolution triangle:
status ignores .claw.json model (#105) +
typo statuss burns tokens (#108) +
-p --model sonnet silently ignored (#117)
Filed in response to Clawhip pinpoint nudge 1494933025857736836
in #clawcode-building-in-public.
|
||
|
|
f2d653896d |
ROADMAP #116: unknown keys in .claw.json hard-fail startup with exit 1; Claude Code migration parity broken (apiKeyHelper rejected); forward-compat impossible; only first error surfaces
Dogfooded 2026-04-18 on main HEAD ad02761 from /tmp/cdRR.
Three related gaps in one finding:
1. Unknown keys are strict ERRORS, not warnings:
{"permissions":{"defaultMode":"default"},"futureField":"x"}
$ claw --output-format json status
# stdout: empty
# stderr: {"type":"error","error":"unknown key futureField"}
# exit: 1
2. Claude Code migration parity broken:
$ cp .claude.json .claw.json
# .claude.json has apiKeyHelper (real Claude Code field)
$ claw --output-format json status
# stderr: unknown key apiKeyHelper → exit 1
No 'this is a Claude Code field we don't support, ignored' message.
3. Only errors[0] is reported — iterative discovery required:
3 unknown fields → 3 edit-run-fix cycles to fix them all.
Error-routing split with --output-format json:
success → stdout
errors → stderr (structured JSON)
Empty stdout on config errors. A claw piping stdout silently
gets nothing. Must capture both streams.
No escape hatch. No --ignore-unknown-config, no --strict flag,
no strictValidation config option.
Trace:
config.rs:282-291 ConfigLoader gate:
let validation = validate_config_file(...);
if !validation.is_ok() {
let first_error = &validation.errors[0];
return Err(ConfigError::Parse(first_error.to_string()));
}
all_warnings.extend(validation.warnings);
config_validate.rs:19-47 DiagnosticKind::UnknownKey:
level: DiagnosticLevel::Error (not Warning)
config_validate.rs schema allow-list is hard-coded. No
forward-compat extension (no x-* reserved namespace, no
additionalProperties: true, no opt-in lax mode).
grep 'apiKeyHelper' rust/crates/runtime/ → 0 matches.
Claude-Code-native fields not tolerated as no-ops.
grep 'ignore.*unknown|--no-validate|strict.*validation'
rust/crates/ → 0 matches. No escape hatch.
Fix shape (~100 lines):
- Downgrade UnknownKey Error → Warning default. ~5 lines.
- Add strict mode flag: .claw.json strictValidation: true OR
--strict-config CLI flag. Default off. ~15 lines.
- Collect all diagnostics, don't halt on first. ~20 lines.
- TOLERATED_CLAUDE_CODE_FIELDS allow-list: apiKeyHelper, env
etc. emit migration-hint warning 'not yet supported; ignored'
instead of hard-fail. ~30 lines.
- Emit structured error envelope on stdout too, not just stderr.
--output-format json stdout includes config_diagnostics[]. ~15.
- Wire suggestion: Option<String> for UnknownKey via fuzzy
match ('permisions' → 'permissions'). ~15 lines.
- Regression tests per outcome.
Joins Claude Code migration parity (#103, #109) as 3rd member —
most severe migration break. #103 silently drops .md files,
#109 stderr-prose warnings, #116 outright hard-fails.
Joins Reporting-surface/config-hygiene (#90, #91, #92, #110,
#115) on error-routing-vs-stdout axis.
Joins Silent-flag/documented-but-unenforced (#96-#101, #104,
#108, #111, #115) — only first error reported, rest silent.
Cross-cluster with Truth-audit — validation.is_ok() hides all
but first structured problem.
Natural bundles:
#103 + #109 + #116 — Claude Code migration parity triangle:
loss of compat (.md dropped) +
loss of structure (stderr prose warnings) +
loss of forward-compat (unknowns hard-fail)
#109 + #116 — config validation reporting surface:
only first warning surfaces structurally (#109)
only first error surfaces structurally AND halts (#116)
Filed in response to Clawhip pinpoint nudge 1494925472239321160
in #clawcode-building-in-public.
|
||
|
|
ad02761918 |
ROADMAP #115: claw init hardcodes 'defaultMode: dontAsk' alias for danger-full-access; init output zero security signal; JSON wraps prose
Dogfooded 2026-04-18 on main HEAD ca09b6b from /tmp/cdPP.
Three compounding issues in one finding:
1. claw init generates .claw.json with dangerous default:
$ claw init && cat .claw.json
{"permissions":{"defaultMode":"dontAsk"}}
$ claw status | grep permission_mode
permission_mode: danger-full-access
2. The 'dontAsk' alias obscures the actual security posture:
config.rs:858 "dontAsk" | "danger-full-access" =>
Ok(ResolvedPermissionMode::DangerFullAccess)
User reads 'dontAsk' as 'skip confirmations I'd otherwise see'
— NOT 'grant every tool unconditional access'. But the two
parse identically. Alias name dilutes severity.
3. claw init --output-format json wraps prose in message field:
{
"kind": "init",
"message": "Init\n Project /private/tmp/cdPP\n
.claw/ created\n..."
}
Claws orchestrating setup must string-parse \n-prose to
know what got created. No files_created[], no
resolved_permission_mode, no security_posture.
Zero mention of 'danger', 'permission', or 'access' anywhere
in init output. The init report says 'Review and tailor the
generated guidance' — implying there's something benign to tailor.
Trace:
rusty-claude-cli/src/init.rs:4-9 STARTER_CLAW_JSON constant:
hardcoded {"permissions":{"defaultMode":"dontAsk"}}
runtime/src/config.rs:858 alias resolution:
"dontAsk" | "danger-full-access" => DangerFullAccess
rusty-claude-cli/src/init.rs:370 JSON-output also emits
'defaultMode': 'dontAsk' literal.
grep 'dontAsk' rust/crates/ → 4 matches. None explain that
dontAsk == danger-full-access anywhere user-facing.
Fix shape (~60 lines):
- STARTER_CLAW_JSON default → 'default' (explicit safe). Users
wanting danger-full-access opt in. ~5 lines.
- init output warns when effective mode is DangerFullAccess:
'security: danger-full-access (unconditional tool approval).'
~15 lines.
- Structure the init JSON:
{kind, files:[{path,action}], resolved_permission_mode,
permission_mode_source, security_warnings:[]}
~30 lines.
- Deprecate 'dontAsk' alias OR log warning at parse: 'alias for
danger-full-access; grants unconditional tool access'. ~8 lines.
- Regression tests per outcome.
Builds on #87 and amplifies it:
#87: absence-of-config default = danger-full-access
#101: fail-OPEN on bad RUSTY_CLAUDE_PERMISSION_MODE env var
#115: init actively generates the dangerous default
Three sequential compounding permission-posture failures.
Joins Permission-audit/tool-allow-list (#94, #97, #101, #106)
as 5th member — init-time anchor of the permission problem.
Joins Silent-flag/documented-but-unenforced on silent-setting
axis. Cross-cluster with Reporting-surface/config-hygiene
(prose-wrapped JSON) and Truth-audit (misleading 'Next step'
phrasing).
Natural bundle: #87 + #101 + #115 — 'permission drift at every
boundary': absence default + env-var bypass + init-generated.
Flagship permission-audit sweep grows 7-way:
#50 + #87 + #91 + #94 + #97 + #101 + #115
Filed in response to Clawhip pinpoint nudge 1494917922076889139
in #clawcode-building-in-public.
|
||
|
|
ca09b6b374 |
ROADMAP #114: /session list and --resume disagree after /clear; reported session_id unresumable; .bak files invisible; 0-byte files fabricate phantoms
Dogfooded 2026-04-18 on main HEAD 43eac4d from /tmp/cdNN and /tmp/cdOO.
Three related findings on session reference resolution asymmetry:
1. /clear divergence (primary):
- /clear --confirm rewrites session_id inside the file header
but reuses the old filename.
- /session list reads meta header, reports new id.
- --resume looks up by filename stem, not meta header.
- Net: /session list reports ids that --resume can't resolve.
Concrete:
claw --resume ses /clear --confirm
→ new_session_id: session-1776481564268-1
→ file still named ses.jsonl, meta session_id now the new id
claw --resume ses /session list
→ active: session-1776481564268-1
claw --resume session-1776481564268-1
→ ERROR session not found
2. .bak files filtered out of /session list silently:
ls .claw/sessions/<bucket>/
ses.jsonl ses.jsonl.before-clear-<ts>.bak
/session list → only ses.jsonl visible, .bak zero discoverability
is_managed_session_file only matches .jsonl and .json.
3. 0-byte session files fabricate phantom sessions:
touch .claw/sessions/<bucket>/emptyses.jsonl
claw --resume emptyses /session list
→ active: session-<ms>-0
→ sessions: [session-<ms>-1]
Two different fabricated ids, neither persisted to disk.
--resume either fabricated id → 'session not found'.
Trace:
session_control.rs:86-116 resolve_reference:
handle.id = session_id_from_path(&path) (filename stem)
.unwrap_or_else(|| ref.to_string())
Meta header NEVER consulted for ref → id mapping.
session_control.rs:118-137 resolve_managed_path:
for ext in [jsonl, json]:
path = sessions_root / '{ref}.{ext}'
if path.exists(): return
Lookup key is filename. Zero fallback to meta scan.
session_control.rs:228-285 collect_sessions_from_dir:
on load success: summary.id = session.session_id (meta)
on load failure: summary.id = path.file_stem() (filename)
/session list thus reports meta ids for good files.
/clear handler rewrites session_id in-place, writes to same
session_path. File keeps old name, gets new id inside.
is_managed_session_file filters .jsonl/.json only. .bak invisible.
Fix shape (~90 lines):
- /clear preserves filename's identity (Option A: keep session_id,
wipe content). /session fork handles new-id semantics (#113).
- resolve_reference falls back to meta-header scan when filename
lookup fails. Covers legacy divergent files.
- /session list surfaces backups via --include-backups flag OR
separate backups: [] array with structured metadata.
- 0-byte session files produce SessionError::EmptySessionFile
instead of silent fabrication. Structured error, not phantom.
- regression tests per failure mode.
Joins Session-handling: #93 + #112 + #113 + #114 — reference
resolution + concurrent-modification + programmatic management +
reference/enumeration asymmetry. Complete session-handling cluster.
Joins Truth-audit — /session list output factually wrong about
what is resumable.
Cross-cluster with Parallel-entry-point asymmetry (#91, #101,
#104, #105, #108) — entry points reading same underlying data
produce mutually inconsistent identifiers.
Natural bundle: #93 + #112 + #113 + #114 (session-handling
quartet — complete coverage).
Alternative bundle: #104 + #114 — /clear filename semantics +
/export filename semantics both hide identity in filename.
Filed in response to Clawhip pinpoint nudge 1494895272936079493
in #clawcode-building-in-public.
|
||
|
|
43eac4d94b |
ROADMAP #113: /session switch/fork/delete unsupported from --resume; no claw session CLI subcommand; REPL-only programmatic gap
Dogfooded 2026-04-18 on main HEAD 8b25daf from /tmp/cdJJ. Test matrix: /session list → works (structured JSON) /session switch s → 'unsupported resumed slash command' /session fork foo → 'unsupported resumed slash command' /session delete s → 'unsupported resumed slash command' /session delete s --force → 'unsupported resumed slash command' claw session delete s → Prompt fallthrough (#108), 'missing credentials' from LLM error path Help documents ALL session verbs as one unified capability: /session [list|switch <session-id>|fork [branch-name]|delete <session-id> [--force]] Summary: 'List, switch, fork, or delete managed local sessions' Implementation: main.rs:10618 parser builds SlashCommand::Session{action, target} for every subverb. All parse successfully. main.rs:2908-2925 dedicated /session list handler. Only one. main.rs:2936-2940+ catch-all: SlashCommand::Session {..} | SlashCommand::Plugins {..} | ... => Err(format_unsupported_resumed_slash_command(...)) main.rs:3963 SlashCommand::Session IS handled in LiveCli REPL path — switch/fork/delete implemented for interactive mode. runtime/session_control.rs:131+ SessionStore::resolve_reference, delete_managed_session, fork_managed_session all exist. grep 'claw session\b' main.rs → zero matches. No CLI subcommand. Gap: backing code exists, parser understands verbs, REPL handler wired — ONLY the --resume dispatch path lacks switch/fork/delete plumbing, and there's no claw session CLI subcommand as programmatic alternative. A claw orchestrating session lifecycle at scale has three options: a) start interactive REPL (impossible without TTY) b) manual .claw/sessions/ rm/cp (bypasses bookkeeping, breaks with #112's proposed locking) c) stick to /session list + /clear, accept missing verbs Fix shape (~130 lines): - /session switch <id> in run_resume_command (~25 lines) - /session fork [branch] in run_resume_command (~30 lines) - /session delete <id> [--force] in run_resume_command (~30), --force required without TTY - claw session <verb> CLI subcommand (~40) - --help: annotate which session verbs are resume-safe vs REPL-only - regression tests per verb x (CLI / slash-via-resume) Joins Unplumbed-subsystem (#78, #96, #100, #102, #103, #107, #109, #111) as 9th declared-but-not-delivered surface. Joins Session- handling (#93, #112) as 3rd member. Cross-cluster with Silent- flag on help-vs-impl mismatch. Natural bundles: #93 + #112 + #113 — session-handling triangle (semantic / concurrency / management API) #78 + #111 + #113 — declared-but-not-delivered triangle with three flavors: #78 fails-noisy (CLI variant → Prompt fallthrough) #111 fails-quiet (slash → wrong handler) #113 no-handler-at-all (slash → unsupported-resumed) Filed in response to Clawhip pinpoint nudge 1494887723818029156 in #clawcode-building-in-public. |
||
|
|
8b25daf915 |
ROADMAP #112: concurrent /compact and /clear race with raw 'No such file or directory (os error 2)' on session file
Dogfooded 2026-04-18 on main HEAD a049bd2 from /tmp/cdII.
5 concurrent /compact on same session → 4 succeed, 1 races with
raw ENOENT. Same pattern with concurrent /clear --confirm.
Trace:
session.rs:204-212 save_to_path:
rotate_session_file_if_needed(path)?
write_atomic(path, &snapshot)?
cleanup_rotated_logs(path)?
Three steps. No lock around sequence.
session.rs:1085-1094 rotate_session_file_if_needed:
metadata(path) → rename(path, rot_path)
Classic TOCTOU. Race window between check and rename.
session.rs:1063-1071 write_atomic:
writes .tmp-{ts}-{counter}, renames to path
Atomic per rename, not per multi-step sequence.
cleanup_rotated_logs deletes .rot-{ts} files older than 3 most
recent. Can race against another process reading that rot file.
No flock, no advisory lock file, no fcntl.
grep 'flock|FileLock|advisory' session.rs → zero matches.
SessionError::Io Display forwards os::Error Display:
'No such file or directory (os error 2)'
No domain translation to 'session file vanished during save'
or 'concurrent modification detected, retry safe'.
Fix shape (~90 lines + test):
- advisory lock: .claw/sessions/<bucket>/<session>.jsonl.lock
exclusive flock for duration of save_to_path (fs2 crate)
- domain error variants:
SessionError::ConcurrentModification {path, operation}
SessionError::SessionFileVanished {path}
- error-to-JSON mapping:
{error_kind: 'concurrent_modification', retry_safe: true}
- retry-policy hints on idempotent ops (/compact, /clear)
- regression test: spawn 10 concurrent /compact, assert all
success OR structured ConcurrentModification (no raw os_error)
Affected operations:
- /compact (session save_to_path after compaction)
- /clear --confirm (save_to_path after new session)
- /export (may hit rotation boundary)
- Turn-persist (append_persisted_message can race rotation)
Not inherently a bug if sessions are single-writer, but
workspace-bucket scoping at session_control.rs:31-32 assumes
one claw per workspace. Parallel ulw lanes, CI matrix runners,
orchestration loops all violate that assumption.
Joins truth-audit (error lies by omission about what happened).
New micro-cluster 'session handling' with #93. Adjacent to
#104 on session-file-handling axis.
Natural bundle: #93 + #112 (session semantic correctness +
concurrency error clarity).
Filed in response to Clawhip pinpoint nudge 1494880177099116586
in #clawcode-building-in-public.
|
||
|
|
a049bd29b1 |
ROADMAP #111: /providers documented as 'List available model providers' but dispatches to Doctor
Dogfooded 2026-04-18 on main HEAD b2366d1 from /tmp/cdHH.
Specification mismatch at the command-dispatch layer:
commands/src/lib.rs:716-720 SlashCommandSpec registry:
name: 'providers', summary: 'List available model providers'
commands/src/lib.rs:1386 parser:
'doctor' | 'providers' => SlashCommand::Doctor
So /providers dispatches to SlashCommand::Doctor. A claw calling
/providers expecting {kind: 'providers', providers: [...]} gets
{kind: 'doctor', checks: [auth, config, install_source, workspace,
sandbox, system]} instead. Same top-level kind field name,
completely different payload.
Help text lies twice:
--help slash listing: '/providers List available model providers'
--help Resume-safe summary: includes /providers
Unlike STUB_COMMANDS (#96) which fail noisily, /providers fails
QUIETLY — returns wrong subsystem output.
Runtime has provider data:
ProviderKind::{Anthropic, Xai, OpenAi, ...} at main.rs:1143-1147
resolve_repl_model with provider-prefix routing
pricing_for_model with per-provider costs
provider_fallbacks config field
Scaffolding is present; /providers just doesn't use it.
By contrast /tokens → Stats and /cache → Stats are semantically
reasonable (Stats has the requested data). /providers → Doctor
is genuinely bizarre.
Fix shape:
A. Implement: SlashCommand::Providers variant + render helper
using ProviderKind + provider_fallbacks + env-var check (~60)
B. Remove: delete 'providers' from registry + parser (~3 lines)
then /providers becomes 'unknown, did you mean /doctor?'
Either way: fix --help to match.
Parallel to #78 (claw plugins CLI variant never constructed,
falls through to prompt). Both are 'declared in spec, not
implemented as declared.' #78 fails noisy, #111 fails quiet.
Joins silent-flag cluster (#96-#101, #104, #108) — 8th
doc-vs-impl mismatch. Joins unplumbed-subsystem (#78, #96,
#100, #102, #103, #107, #109) as 8th declared-but-not-
delivered surface. Joins truth-audit.
Natural bundles:
#78 + #96 + #111 — declared-but-not-as-declared triangle
#96 + #108 + #111 — full --help/dispatch hygiene quartet
(help-filter-leaks + subcommand typo fallthrough + slash
mis-dispatch)
Filed in response to Clawhip pinpoint nudge 1494872623782301817
in #clawcode-building-in-public.
|
||
|
|
b2366d113a |
ROADMAP #110: ConfigLoader only checks cwd paths; .claw.json at project_root invisible from subdirectories
Dogfooded 2026-04-18 on main HEAD 16244ce from /tmp/cdGG/nested/deep/dir.
ConfigLoader::discover at config.rs:242-270 hardcodes every
project/local path as self.cwd.join(...):
- self.cwd.join('.claw.json')
- self.cwd.join('.claw').join('settings.json')
- self.cwd.join('.claw').join('settings.local.json')
No ancestor walk. No consultation of project_root.
Concrete:
cd /tmp/cdGG && git init && echo '{permissions:{defaultMode:read-only}}' > .claw.json
cd /tmp/cdGG/nested/deep/dir
claw status → permission_mode: 'danger-full-access' (fallback)
claw doctor → 'Config files loaded 0/0, defaults are active'
But project_root: /tmp/cdGG is correctly detected via git walk.
Same config file, same repo, invisible from subdirectory.
Meanwhile CLAUDE.md discovery walks ancestors unbounded (per #85
over-discovery). Same subsystem category, opposite policy, no doc.
Security-adjacent per #87: permission-mode fallback is
danger-full-access. cd'ing to a subdirectory silently upgrades
from read-only (configured) → danger-full-access (fallback) —
workspace-location-dependent permission drift.
Fix shape (~90 lines):
- add project_root_for(&cwd) helper (reuse git-root walker from
render_doctor_report)
- config search: user → project_root/.claw.json →
project_root/.claw/settings.json → cwd/.claw.json (overlay) →
cwd/.claw/settings.* (overlays)
- optionally walk intermediate ancestors
- surface 'where did my config come from' in doctor (pairs with
#106 + #109 provenance)
- warn when cwd has no config but project_root does
- documentation parity with CLAUDE.md
- regression tests per cwd depth + overlay precedence
Joins truth-audit (doctor says 'ok, defaults active' when config
exists). Joins discovery-overreach as opposite-direction sibling:
#85: skills ancestor walk UNBOUNDED (over-discovery)
#88: CLAUDE.md ancestor walk enables injection
#110: config NO ancestor walk (under-discovery)
Natural bundle: #85 + #110 (ancestor policy unification), or
#85 + #88 + #110 (full three-way ancestor-walk audit).
Filed in response to Clawhip pinpoint nudge 1494865079567519834
in #clawcode-building-in-public.
|
||
|
|
16244cec34 |
ROADMAP #109: config validation warnings stderr-only; structured ConfigDiagnostic flattened to prose, JSON-invisible
Dogfooded 2026-04-18 on main HEAD 21b2773 from /tmp/cdDD.
Validator produces structured diagnostics but loader discards
them after stderr eprintln:
config_validate.rs:19-66 ConfigDiagnostic {path, field, line,
kind: UnknownKey|WrongType|Deprecated}
config_validate.rs:313-322 DEPRECATED_FIELDS: permissionMode,
enabledPlugins
config_validate.rs:451 emits DiagnosticKind::Deprecated
config.rs:285-300 ConfigLoader::load:
if !validation.is_ok() {
return Err(validation.errors[0].to_string()) // ERRORS propagate
}
all_warnings.extend(validation.warnings);
for warning in &all_warnings {
eprintln!('warning: {warning}'); // WARNINGS stderr only
}
RuntimeConfig has no warnings field. No accessor. No route from
validator structured data to doctor/status JSON envelope.
Concrete:
.claw.json with enabledPlugins:{foo:true}
→ config check: {status: 'ok', summary: 'runtime config
loaded successfully'}
→ stderr: 'warning: field enabledPlugins is deprecated'
→ claw with 2>/dev/null loses the warning entirely
Errors DO propagate correctly:
.claw.json with 'permisions' (typo)
→ config check: {status: 'fail', summary: 'unknown key
permisions... Did you mean permissions?'}
Warning→stderr, Error→JSON asymmetry: a claw reading JSON can
see errors structurally but can't see warnings at all. Silent
migration drift: legacy claude-code 'permissionMode' key still
works, warning lost, operator never sees 'use permissions.
defaultMode' guidance unless they notice stderr.
Fix shape (~85 lines, all additive):
- add warnings: Vec<ConfigDiagnostic> field to RuntimeConfig
- populate from all_warnings, keep eprintln for human ops
- add ConfigDiagnostic::to_json_value emitting
{path, field, line, kind, message, replacement?}
- check_config_health: status='warn' + warnings[] JSON when
non-empty
- surface in status JSON (config_warnings[] or top-level
warnings[])
- surface in /config slash-command output
- regression tests per deprecated field + aggregation + no-warn
Joins truth-audit (#80-#87, #89, #100, #102, #103, #105, #107)
— doctor says 'ok' while validator flagged deprecations. Joins
unplumbed-subsystem (#78, #96, #100, #102, #103, #107) — 7th
surface. Joins Claude Code migration parity (#103) —
permissionMode legacy path is stderr-only.
Natural bundles:
#100 + #102 + #103 + #107 + #109 — 5-way doctor-surface
coverage plus structured warnings (doctor stops lying PR)
#107 + #109 — stderr-only-prose-warning sweep (hook events +
config warnings = same plumbing pattern)
Filed in response to Clawhip pinpoint nudge 1494857528335532174
in #clawcode-building-in-public.
|
||
|
|
21b2773233 |
ROADMAP #108: subcommand typos silently fall through to LLM prompt dispatch, burning billed tokens
Dogfooded 2026-04-18 on main HEAD 91c79ba from /tmp/cdCC.
Unrecognized first-positional tokens fall through the
_other => Ok(CliAction::Prompt { ... }) arm at main.rs:707.
Per --help this is 'Shorthand non-interactive prompt mode' —
documented behavior — but it eats known-subcommand typos too:
claw doctorr → Prompt("doctorr") → LLM API call
claw skilsl → Prompt("skilsl") → LLM API call
claw statuss → Prompt("statuss") → LLM API call
claw deply → Prompt("deply") → LLM API call
With credentials set, each burns real tokens. Without creds,
returns 'missing Anthropic credentials' — indistinguishable
from a legitimate prompt failure. No 'did you mean' suggestion.
Infrastructure exists:
slash command typos:
claw --resume s /skilsl
→ 'Unknown slash command: /skilsl. Did you mean /skill, /skills'
flag typos:
claw --fake-flag
→ structured error 'unknown option: --fake-flag'
subcommand typos:
→ silently become LLM prompts
The did-you-mean helper exists for slash commands. Flag
validation exists. Only subcommand dispatch has the silent-
fallthrough.
Fix shape (~60 lines):
- suggest_similar_subcommand(token) using levenshtein ≤ 2
against the ~16-item known-subcommand list
- gate the Prompt fallthrough on a shape heuristic:
single-token + near-match → return structured error with
did-you-mean. Otherwise fall through unchanged.
- preserve shorthand-prompt mode for multi-word inputs,
quoted inputs, and non-near-match tokens
- regression tests per typo shape + legit prompt + quoted
workaround
Cross-claw orchestration hazard: claws constructing subcommand
names from config or other claws' output have a latent 'typo →
live LLM call' vector. Over CI matrix with 1% typo rate, that's
billed-token waste + structural signal loss (error handler
can't distinguish typo from legit prompt failure).
Joins silent-flag cluster (#96-#101, #104) on subcommand axis —
6th instance of 'malformed input silently produces unintended
behavior.' Joins parallel-entry-point asymmetry (#91, #101,
#104, #105) — slash vs subcommand disagree on typo handling.
Natural bundles: #96 + #98 + #108 (--help/dispatch surface
hygiene triangle), #91 + #101 + #104 + #105 + #108 (parallel-
entry-point 5-way).
Filed in response to Clawhip pinpoint nudge 1494849975530815590
in #clawcode-building-in-public.
|
||
|
|
91c79baf20 |
ROADMAP #107: hooks subsystem fully invisible to JSON diagnostic surfaces; doctor no hook check, /hooks is stub, progress events stderr-only
Dogfooded 2026-04-18 on main HEAD a436f9e from /tmp/cdBB. Complete hook invisibility across JSON diagnostic surfaces: 1. doctor: no check_hooks_health function exists. check_config_health emits 'Config files loaded N/M, MCP servers N, Discovered file X' — NO hook count, no hook event breakdown, no hook health. .claw.json with 3 hooks (including /does/not/exist and curl-pipe-sh remote-exec payload) → doctor: ok, has_failures: false. 2. /hooks list: in STUB_COMMANDS (main.rs:7272) → returns 'not yet implemented in this build'. Parallel /mcp list / /agents list / /skills list work fine. /hooks has no sibling. 3. /config hooks: reports loaded_files and merged_keys but NOT hook bodies, NOT hook source files, NOT per-event breakdown. 4. Hook progress events route to eprintln! as prose: CliHookProgressReporter (main.rs:6660-6695) emits '[hook PreToolUse] tool_name: command' to stderr unconditionally. NEVER into --output-format json. A claw piping stderr to /dev/null (common in pipelines) loses all hook visibility. 5. parse_optional_hooks_config_object (config.rs:766) accepts any non-empty string. No fs::metadata() check, no which() check, no shell-syntax sanity check. 6. shell_command (hooks.rs:739-754) runs 'sh -lc <command>' with full shell expansion — env vars, globs, pipes, , remote curl pipes. Compounds with #106: downstream .claw/settings.local.json can silently replace the entire upstream hook array via the deep_merge_objects replace-semantic. A team-level audit hook in ~/.claw/settings.json is erasable and replaceable by an attacker-controlled hook with zero visibility anywhere machine-readable. Fix shape (~220 lines, all additive): - check_hooks_health doctor check (like #102's check_mcp_health) - status JSON exposes {pre_tool_use, post_tool_use, post_tool_use_failure} with source-file provenance - implement /hooks list (remove from STUB_COMMANDS) - route HookProgressEvent into JSON turn-summary as hook_events[] - validate hook commands at config-load, classify execution_kind - regression tests Joins truth-audit (#80-#87, #89, #100, #102, #103, #105) — doctor lies when hooks are broken or hostile. Joins unplumbed-subsystem (#78, #96, #100, #102, #103) — HookProgressEvent exists, JSON-invisible. Joins subsystem-doctor-coverage (#100, #102, #103) as fourth opaque subsystem. Cross-cluster with permission-audit (#94, #97, #101, #106) because hooks ARE a permission mechanism. Natural bundle: #102 + #103 + #107 (subsystem-doctor-coverage 3-way becomes 4-way). Plus #106 + #107 (policy-erasure + policy- visibility = complete hook-security story). Filed in response to Clawhip pinpoint nudge 1494834879127486544 in #clawcode-building-in-public. |
||
|
|
a436f9e2d6 |
ROADMAP #106: config merge deep_merge_objects REPLACES arrays; permission deny rules can be silently erased by downstream config layer
Dogfooded 2026-04-18 on main HEAD 71e7729 from /tmp/cdAA.
deep_merge_objects at config.rs:1216-1230 recurses into nested
objects but REPLACES arrays. So:
~/.claw/settings.json: {"permissions":{"deny":["Bash(rm *)"]}}
.claw.json: {"permissions":{"deny":["Bash(sudo *)"]}}
Merged: {"permissions":{"deny":["Bash(sudo *)"]}}
User's Bash(rm *) deny rule SILENTLY LOST. No warning. doctor: ok.
Worst case:
~/.claw/settings.json: {deny: [...strict list...]}
.claw/settings.local.json: {deny: []}
Merged: {deny: []}
Every deny rule from every upstream layer silently removed by a
workspace-local file. Any team/org security policy distributed
via user-home config is trivially erasable.
Arrays affected:
permissions.allow/deny/ask
hooks.PreToolUse/PostToolUse/PostToolUseFailure
plugins.externalDirectories
MCP servers are merged BY-KEY (merge_mcp_servers at :709) so
distinct server names across layers coexist. Author chose
merge-by-key for MCP but not for policy arrays. Design is
internally inconsistent.
extend_unique + push_unique helpers EXIST at :1232-1244 that do
union-merge with dedup. They are not called on the config-merge
axis for any policy array.
Fix shape (~100 lines):
- union-merge permissions.allow/deny/ask via extend_unique
- union-merge hooks.* arrays
- union-merge plugins.externalDirectories
- explicit replace-semantic opt-in via 'deny!' sentinel or
'permissions.replace: [...]' form (opt-in, not default)
- doctor surfaces policy provenance per rule (also helps #94)
- emit warning when replace-sentinel is used
- regression tests for union + explicit replace + multi-layer
Joins permission-audit sweep as 4-way composition-axis finding
(#94, #97, #101, #106). Joins truth-audit (doctor says 'ok'
while silently deleted every deny rule).
Natural bundle: #94 + #106 (rule validation + rule composition).
Plus #91 + #94 + #97 + #101 + #106 as 5-way policy-surface-audit.
Filed in response to Clawhip pinpoint nudge 1494827325085454407
in #clawcode-building-in-public.
|
||
|
|
71e77290b9 |
ROADMAP #105: claw status ignores .claw.json model, doctor mislabels alias as Resolved, 4 surfaces disagree
Dogfooded 2026-04-18 on main HEAD 6580903 from /tmp/cdZ.
.claw.json with {"model":"haiku"} produces:
claw status → model: 'claude-opus-4-6' (DEFAULT_MODEL, config ignored)
claw doctor → 'Resolved model haiku' (raw alias, label lies)
turn dispatch → claude-haiku-4-5-20251213 (actually-resolved canonical)
ANTHROPIC_MODEL=sonnet → status still says claude-opus-4-6
FOUR separate understandings of 'active model':
1. config file (alias as written)
2. doctor (alias mislabeled as 'Resolved')
3. status (hardcoded DEFAULT_MODEL ignoring config entirely)
4. turn dispatch (canonical, alias-resolved, what turns actually use)
Trace:
main.rs:59 DEFAULT_MODEL const = claude-opus-4-6
main.rs:400 parse_args starts model = DEFAULT_MODEL
main.rs:753 Status dispatch: model.to_string() — never calls
resolve_repl_model, never reads config or env
main.rs:1125 resolve_repl_model: source of truth for actual
model, consults ANTHROPIC_MODEL env + config + alias table.
Called from Prompt and Repl dispatch. NOT from Status.
main.rs:1701 check_config_health: 'Resolved model {model}'
where model is raw configured string, not resolved.
Label says Resolved, value is pre-resolution alias.
Orchestration hazard: a claw picks tool strategy based on
status.model assuming it reflects what turns will use. Status
lies: always reports DEFAULT_MODEL unless --model flag was
passed. Config and env var completely ignored by status.
Fix shape (~30 lines):
- call resolve_repl_model from print_status_snapshot
- add effective_model field to status JSON (or rename/enrich)
- fix doctor 'Resolved model' label (either rename to 'Configured'
or actually alias-resolve before emitting)
- honor ANTHROPIC_MODEL env in status
- regression tests per model source with cross-surface equality
Joins truth-audit (#80-#84, #86, #87, #89, #100, #102, #103).
Joins two-paths-diverge (#91, #101, #104) — now 4-way with #105.
Joins doctor-surface-coverage triangle (#100 + #102 + #105).
Filed in response to Clawhip pinpoint nudge 1494819785676947543
in #clawcode-building-in-public.
|
||
|
|
6580903d20 |
ROADMAP #104: /export and claw export are two paths with incompatible filename semantics; slash silently .txt-rewrites
Dogfooded 2026-04-18 on main HEAD 7447232 from /tmp/cdY.
Two-path-diverge problem:
A. /export slash command (resolve_export_path at main.rs:5990-6010):
- If extension != 'txt', silently appends '.txt'
- /export foo.md → writes foo.md.txt
- /export report.json → writes report.json.txt
- cwd.join(relative_path_with_dotdot) resolves outside cwd
- No path-traversal rejection
B. claw export CLI (run_export at main.rs:6021-6055):
- fs::write(path, &markdown) directly, no suffix munging
- /tmp/cli-export.md → writes /tmp/cli-export.md
- Also no path-traversal check, absolute paths write wherever
Same logical action, incompatible output contracts. A claw that
switches between /export and claw export sees different output
filenames for the same input.
Compounded:
- Content is Markdown (render_session_markdown emits '# Conversation
Export', '## 1. User', fenced code blocks) but slash path forces
.txt extension → content/extension mismatch. File-routing
pipelines (archival by extension, syntax highlight, preview)
misclassify.
- --help says just '/export [file]'. No mention of .txt forcing,
no mention of path-resolution semantics.
- Claw pipelines that glob *.md won't find /export outputs.
Trace:
main.rs:5990 resolve_export_path: extension check + conditional
.txt append
main.rs:6021 run_export: fs::write direct, no path munging
main.rs:5975 default_export_filename: hardcodes .txt fallback
Content renderer is Markdown (render_session_markdown:6075)
Fix shape (~70 lines):
- unify both paths via shared export_session_to_path helper
- respect caller's extension (pick renderer by extension or
accept that content is Markdown and name accordingly)
- path-traversal policy decision: restrict to project root or
allow-with-warning
- --help: document suffix preservation + path semantics
- regression tests for extension preservation + dotdot rejection
Joins silent-flag cluster (#96-#101) on silent-rewrite axis.
New two-paths-diverge sub-cluster: #91 (permission-mode parser
disagree) + #101 (CLI vs env asymmetry) + #104 (slash vs CLI
export asymmetry) — three instances of parallel entry points
doing subtly different things.
Natural bundles: #91 + #101 + #104 (two-paths-diverge trio),
#96 + #98 + #99 + #101 + #104 (silent-rewrite-or-noop quintet).
Filed in response to Clawhip pinpoint nudge 1494812230372294849
in #clawcode-building-in-public.
|
||
|
|
7447232688 |
ROADMAP #103: claw agents silently drops every non-.toml file; claude-code convention .md files ignored, no content validation
Dogfooded 2026-04-18 on main HEAD 6a16f08 from /tmp/cdX. Two-part gap on agent subsystem: 1. File-format gate silently discards .md (YAML frontmatter): commands/src/lib.rs:3180-3220 load_agents_from_roots filters extension() != 'toml' and silently continues. No log, no warn. .claw/agents/foo.md → agents list count: 0, doctor: ok. Same file renamed to .toml → discovered instantly. 2. No content validation inside accepted .toml: model='nonexistent/model-that-does-not-exist' → accepted. tools=['DoesNotExist', 'AlsoFake'] → accepted. reasoning_effort string → unvalidated. No check against model registry, tool registry, or reasoning-effort enum — all machinery exists elsewhere (#97 validates tools for --allowedTools flag). Compounded: - agents help JSON lists sources but NOT accepted file formats. Operators have zero documentation-surface way to diagnose 'why does my .md file not work?' - Doctor check set has no agents check. 3 files present with 1 silently skipped → summary: 'ok'. - Skills use .md (SKILL.md). MCP uses .json (.claw.json). Agents uses .toml. Three subsystems, three formats, no cross-subsystem consistency or documentation. - Claude Code convention is .md with YAML frontmatter. Migrating operators copy that and silently fail. Fix shape (~100 lines): - accept .md with YAML frontmatter via existing parse_skill_frontmatter helper - validate model/tools/reasoning_effort against existing registries; emit status: 'invalid' + validation_errors instead of silently accepting - agents list summary.skipped: [{path, reason}] - add agents doctor check (total/active/skipped/invalid) - agents help: accepted_formats list Joins truth-audit (#80-#84, #86, #87, #89, #100, #102) on silent-ok-while-ignoring axis. Joins silent-flag (#96-#101) at subsystem scale. Joins unplumbed-subsystem (#78, #96, #100, #102) as 5th unreachable surface: load_agents_from_roots present, parse_skill_frontmatter present, validation helpers present, agents path calls none of them. Also opens new 'Claude Code migration parity' cross-cluster: claw-code silently breaks the expected convention migration path for a first-class subsystem. Natural bundles: #102 + #103 (subsystem-doctor-coverage), #78 + #96 + #100 + #102 + #103 (unplumbed-surface quintet). Filed in response to Clawhip pinpoint nudge 1494804679962661187 in #clawcode-building-in-public. |
||
|
|
6a16f0824d |
ROADMAP #102: mcp list/show/doctor surface MCP config-time only; no preflight, no liveness, not even command-exists check
Dogfooded 2026-04-18 on main HEAD eabd257 from /tmp/cdW2.
A .claw.json pointing at command='/does/not/exist' as an MCP server
cheerfully reports:
mcp show unreachable → found: true
mcp list → configured_servers: 1, status field absent
doctor → config: ok, MCP servers: 1, has_failures: false
The broken server is invisible until agent tries to call a tool
from it mid-turn — burning tokens on failed tool call and forcing
retry loop.
Trace:
main.rs:1701-1780 check_config_health counts via
runtime_config.mcp().servers().len()
No which(). No TcpStream::connect(). No filesystem touch.
render_doctor_report has 6 checks (auth/config/install_source/
workspace/sandbox/system). No check_mcp_health exists.
commands/src/lib.rs mcp list/show emit config-side repr only.
No status field, no reachable field, no startup_state.
runtime/mcp_stdio.rs HAS startup machinery with error types,
but only invoked at turn-execution time — too late for
preflight.
Roadmap prescribes this exact surface:
- Phase 1 §3.5 Boot preflight / doctor contract explicitly lists
'MCP config presence and server reachability expectations'
- Phase 2 §4 canonical lane event schema includes lane.ready
- Phase 4.4.4 event provenance / environment labeling
- Product Principle #5 'Partial success is first-class' —
'MCP startup can succeed for some servers and fail for
others, with structured degraded-mode reporting'
All four unimplementable without preflight + per-server status.
Fix shape (~110 lines):
- check_mcp_health: which(command) for stdio, 1s TcpStream
connect for http/sse. Aggregate ok/warn/fail with per-server
detail lines.
- mcp list/show: add status field
(configured/resolved/command_not_found/connect_refused/
startup_failed). --probe flag for deeper handshake.
- doctor top-level: degraded_mode: bool, startup_summary.
- Wire preflight into prompt/repl bootstrap; emit one-time
mcp_preflight event.
Joins unplumbed-subsystem cross-cluster (#78, #100, #102) —
subsystem exists, diagnostic surface JSON-invisible. Joins
truth-audit (#80-#84, #86, #87, #89, #100) — doctor: ok lies
when MCP broken.
Natural bundle: #78 + #96 + #100 + #102 unplumbed-surface
quartet. Also #100 + #102 as pure doctor-surface-coverage 2-way.
Filed in response to Clawhip pinpoint nudge 1494797126041862285
in #clawcode-building-in-public.
|
||
|
|
eabd257968 |
ROADMAP #101: RUSTY_CLAUDE_PERMISSION_MODE env var silently fails OPEN to danger-full-access on any invalid value
Dogfooded 2026-04-18 on main HEAD d63d58f from /tmp/cdV.
Qualitatively worse than #96-#100 silent-flag class because this
is fail-OPEN, not fail-inert: operator intent 'restrict this lane'
silently becomes 'full access.'
Tested matrix:
VALID → correct mode:
read-only → read-only
workspace-write → workspace-write
danger-full-access → danger-full-access
' read-only ' → read-only (trim works)
INVALID → silent danger-full-access:
'' → danger-full-access
'readonly' → danger-full-access (typo: missing hyphen)
'read_only' → danger-full-access (typo: underscore)
'READ-ONLY' → danger-full-access (case)
'ReadOnly' → danger-full-access (case)
'dontAsk' → danger-full-access (config alias not recognized by env parser, but ultimate default happens to be dfa)
'garbage' → danger-full-access (pure garbage)
'readonly\n' → danger-full-access
CLI asymmetry: --permission-mode readonly → loud structured error.
Same misspelling, same input, opposite outcomes via env vs CLI.
Trace:
main.rs:1099-1107 default_permission_mode:
env::var(...).ok().and_then(normalize_permission_mode)
.or_else(config...).unwrap_or(DangerFullAccess)
→ .and_then drops error context on invalid;
.unwrap_or fail-OPEN to most permissive mode
main.rs:5455-5462 normalize_permission_mode accepts 3 canonical;
runtime/config.rs:855-863 parse_permission_mode_label accepts 7
including config aliases (default/plan/acceptEdits/auto/dontAsk).
Two parsers, disagree on accepted set, no shared source of truth.
Plus: env var RUSTY_CLAUDE_PERMISSION_MODE is UNDOCUMENTED.
grep of README/docs/help returns zero hits.
Fix shape (~60 lines total):
- rewrite default_permission_mode to surface invalid values via Result
- share ONE parser across CLI/config/env (extract from config.rs:855)
- decide broad (7 aliases) vs narrow (3 canonical) accepted set
- document the env var in --help Environment section
- add doctor check surfacing permission_mode.source attribution
- optional: rename to CLAW_PERMISSION_MODE with deprecation alias
Joins permission-audit sweep (#50/#87/#91/#94/#97/#101) on the env
axis. Completes the three-way input-surface audit: CLI + config +
env. Cross-cluster with silent-flag #96-#100 (worse variant: fail-OPEN)
and truth-audit (#80-#87, #89, #100) (operator can't verify source).
Natural 6-way bundle: #50 + #87 + #91 + #94 + #97 + #101 closes the
entire permission-input attack surface in one pass.
Filed in response to Clawhip pinpoint nudge 1494789577687437373
in #clawcode-building-in-public.
|
||
|
|
d63d58f3d0 |
ROADMAP #100: claw status/doctor JSON expose no commit identity; stale-base subsystem unplumbed
Dogfooded 2026-04-18 on main HEAD 63a0d30 from /tmp/cdU + /tmp/cdO*. Three-fold gap: 1. status/doctor JSON workspace object has 13 fields; none of them contain: head_sha, head_short_sha, expected_base, base_source, stale_base_state, upstream, ahead, behind, merge_base, is_detached, is_bare, is_worktree. A claw cannot answer 'is this lane at the expected base?' from the JSON surface alone. 2. --base-commit flag is silently accepted by status/doctor/sandbox/ init/export/mcp/skills/agents and silently dropped on dispatch. Same silent-no-op class as #98. A claw running 'claw --base-commit $expected status' gets zero effect — flag parses into a local, discharged at dispatch. 3. runtime::stale_base subsystem is FULLY implemented with 30+ tests (BaseCommitState, BaseCommitSource, resolve_expected_base, read_claw_base_file, check_base_commit, format_stale_base_warning). run_stale_base_preflight at main.rs:3058 calls it from Prompt/Repl only, writes output to stderr as human prose. .claw-base file is honored internally but invisible to status/doctor JSON. Complete implementation, wrong dispatch points. Plus: detached HEAD reported as magic string 'git_branch: "detached HEAD"' without accompanying SHA. Bare repo/worktree/submodule indistinguishable from regular repo in JSON. parse_git_status_branch has latent dot-split truncation bug on branch names like 'feat.ui' with upstream. Hits roadmap Product Principle #4 (Branch freshness before blame) and Phase 2 §4.2 (branch.stale_against_main event) directly — both unimplementable without commit identity in the JSON surface. Fix shape (~80 lines plumbing): - add head_sha/head_short_sha/is_detached/head_ref/is_bare/is_worktree - add base_commit: {source, expected, state} - add upstream: {ref, ahead, behind, merge_base} - wire --base-commit into CliAction::Status + CliAction::Doctor - add stale_base doctor check - fix parse_git_status_branch dot-split at :2541 Cross-cluster: truth-audit/diagnostic-integrity (#80-#87, #89) + silent-flag (#96-#99) + unplumbed-subsystem (#78). Natural bundles: #89+#100 (git-state completeness) and #78+#96+#100 (unplumbed surface). Milestone: ROADMAP #100. Filed in response to Clawhip pinpoint nudge 1494782026660712672 in #clawcode-building-in-public. |
||
|
|
63a0d30f57 |
ROADMAP #99: claw system-prompt --cwd/--date unvalidated, prompt-injection via newline
Dogfooded 2026-04-18 on main HEAD 0e263be from /tmp/cdN.
parse_system_prompt_args at main.rs:1162-1190 does:
cwd = PathBuf::from(value);
date.clone_from(value);
Zero validation. Both values flow through to
SystemPromptBuilder::render_env_context (prompt.rs:175-186) and
render_project_context (prompt.rs:289-293) where they are formatted
into the system prompt output verbatim via format!().
Two injection points per value:
- # Environment context
- 'Working directory: {cwd}'
- 'Date: {date}'
- # Project context
- 'Working directory: {cwd}'
- 'Today's date is {date}.'
Demonstrated attacks:
--date 'not-a-date' → accepted
--date '9999-99-99' → accepted
--date '1900-01-01' → accepted
--date "2025-01-01'; DROP TABLE users;--" → accepted verbatim
--date $'2025-01-01\nMALICIOUS: ignore all previous rules'
→ newline breaks out of bullet into standalone system-prompt
instruction line that the LLM will read as separate guidance
--cwd '/does/not/exist' → silently accepted, rendered verbatim
--cwd '' → empty 'Working directory: ' line
--cwd $'/tmp\nMALICIOUS: pwn' → newline injection same pattern
--help documents format as '[--cwd PATH] [--date YYYY-MM-DD]'.
Parser enforces neither. Same class as #96 / #98 — documented
constraint, unenforced at parse boundary.
Severity note: most severe of the #96/#97/#98/#99 silent-flag
class because the failure mode is prompt injection, not a silent
feature no-op. A claw or CI pipeline piping tainted
$REPO_PATH / $USER_INPUT into claw system-prompt is a
vector for LLM manipulation.
Fix shape:
1. parse --date as chrono::NaiveDate::parse_from_str(value, '%Y-%m-%d')
2. validate --cwd via std::fs::canonicalize(value)
3. defense-in-depth: debug_assert no-newlines at render boundary
4. regression tests for each rejected case
Cross-cluster: sibling of #83 (system-prompt date = build date)
and #84 (dump-manifests bakes abs path) — all three are about
the system-prompt / manifest surface trusting compile-time or
operator-supplied values that should be validated.
Filed in response to Clawhip pinpoint nudge 1494774477009981502
in #clawcode-building-in-public.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
d0de86e8bc |
ROADMAP #91: permission-mode parsers disagree; dontAsk silently means danger-full-access
Dogfooded 2026-04-18 on main HEAD 478ba55 from /tmp/cdC. Two
permission-mode parsers disagree on valid labels:
- Config parse_permission_mode_label (runtime/src/config.rs:851-862)
accepts 8 labels and collapses 5 aliases onto 3 canonical modes.
- CLI normalize_permission_mode (rusty-claude-cli/src/main.rs:5455-
5461) accepts only the 3 canonical labels.
Same binary, same intent, opposite verdicts:
.claw.json {"defaultMode":"plan"} -> silent ReadOnly + doctor ok
--permission-mode plan -> rejected with 'unsupported permission mode'
Semantic collapses of note:
- 'default' -> ReadOnly (name says nothing about what default means)
- 'plan' -> ReadOnly (upstream plan-mode semantics don't exist in
claw; ExitPlanMode tool exists but has no matching PermissionMode
variant)
- 'acceptEdits'/'auto' -> WorkspaceWrite (ambiguous names)
- 'dontAsk' -> DangerFullAccess (FOOTGUN: sounds like 'quiet mode',
actually the most permissive; community copy-paste bypasses every
danger-keyword audit)
Status JSON exposes canonicalized permission_mode only; original
label lost. Claw reading status cannot distinguish 'plan' from
explicit 'read-only', or 'dontAsk' from explicit 'danger-full-access'.
Fix shape (~20-30 lines): align the two parsers to accept/reject
identical labels; add permission_mode_raw to status JSON (paired
with permission_mode_source from #87); either remove the 'dontAsk'
alias or trigger a doctor warn when raw='dontAsk'; optionally
introduce a real PermissionMode::Plan runtime variant.
Filed in response to Clawhip pinpoint nudge 1494714078965403848 in
#clawcode-building-in-public.
|
||
|
|
478ba55063 |
ROADMAP #90: claw mcp surface redacts env but dumps args/url/headersHelper
Dogfooded 2026-04-17 on main HEAD 64b29f1 from /tmp/cdB. The MCP
details surface correctly redacts env -> env_keys and headers ->
header_keys (deliberate precedent for 'show config without secrets'),
but dumps args, url, and headersHelper verbatim even though all
three standardly carry inline credentials.
Repros:
(1) args leak: {'args':['--api-key','sk-secret-ABC123','--token=...',
'--url=https://user:password@host/db']} appears unredacted in
both details.args and the summary string.
(2) URL leak: 'url':'https://user:SECRET@api.example.com/mcp' and
matching summary.
(3) headersHelper leak: helper command path + its secret-bearing
argv emitted whole.
Trace: mcp_server_details_json at commands/src/lib.rs:3972-3999 is
the single redaction point. env/headers get key-only projection;
args/url/headers_helper carve-out with no explaining comment. Text
surface at :3873-3920 mirrors the same leak.
Clawability shape: mcp list --output-format json is exactly the
surface orchestrators scrape for preflight and that logs / Discord
announcements / claw export / CI artifacts will carry. Asymmetric
redaction sends the wrong signal -- consumers assume secret-aware,
the leak is unexpected and easy to miss. Standard MCP wiring
patterns (--api-key, postgres://user:pass@, token helper scripts)
all hit the leak.
Fix shape (~40-60 lines): redact args with secret heuristic
(--api-key, --token, --password, high-entropy tails, user:pass@);
redact URL basic-auth + query-string secrets; split headersHelper
argv and apply args heuristic; add optional --show-sensitive
opt-in; add mcp_secret_posture doctor check. No MCP runtime
behavior changes -- only reporting surface.
Filed in response to Clawhip pinpoint nudge 1494706529918517390 in
#clawcode-building-in-public.
|
||
|
|
64b29f16d5 |
ROADMAP #89: claw blind to mid-rebase/merge/cherry-pick git states
Dogfooded 2026-04-17 on main HEAD 9882f07. A rebase halted on
conflict leaves .git/rebase-merge/ on disk + HEAD detached on the
rebase intermediate commit. 'claw --output-format json status'
reports git_state='dirty ... 1 conflicted', git_branch='detached
HEAD', no rebase flag. 'claw --output-format json doctor' reports
workspace: {status:ok, summary:'project root detected on branch
detached HEAD'}.
Trace: parse_git_workspace_summary at rusty-claude-cli/src/main.rs:
2550-2587 scans git status --short output only; no .git/rebase-
merge, .git/rebase-apply, .git/MERGE_HEAD, .git/CHERRY_PICK_HEAD,
.git/BISECT_LOG check anywhere in rust/crates/. check_workspace_
health emits Ok so long as a project root was detected.
Clawability impact: preflight blindness (doctor ok on paused lane),
stale-branch detection breaks (freshness vs base is meaningless
when HEAD is a rebase intermediate), no recovery surface (no
abort/resume hints), same 'surface lies about runtime truth' family
as #80-#87.
Fix shape (~20 lines): detect marker files, expose typed
workspace.git_operation field (kind/paused/abort_hint/resume_hint),
flip workspace doctor verdict to warn when git_operation != null.
Filed in response to Clawhip pinpoint nudge 1494698980091756678 in
#clawcode-building-in-public.
|
||
|
|
9882f07e7d |
ROADMAP #88: unbounded CLAUDE.md ancestor walk = prompt injection via /tmp
Dogfooded 2026-04-17 on main HEAD 82bd8bb from /tmp/claude-md-injection/inner/work. discover_instruction_files at runtime/src/prompt.rs:203-224 walks cursor.parent() until None with no project-root bound, no HOME containment, no git boundary. Four candidate paths per ancestor (CLAUDE.md, CLAUDE.local.md, .claw/CLAUDE.md, .claw/instructions.md) are loaded and inlined verbatim into the agent's system prompt under '# Claude instructions'. Repro: /tmp/claude-md-injection/CLAUDE.md containing adversarial guidance appears under 'CLAUDE.md (scope: /private/tmp/claude-md- injection)' in claw system-prompt from any nested CWD. git init inside the worker does not terminate the walk. /tmp/CLAUDE.md alone is sufficient -- /tmp is world-writable with sticky bit on macOS/ Linux, so any local user can plant agent guidance for every other user's claw invocation under /tmp/anything. Worse than #85 (skills ancestor walk): no agent action required (injection fires on every turn before first user message), lower bar for the attacker (raw Markdown, no frontmatter), standard world-writable drop point (/tmp), no doctor signal. Same structural fix family though: prompt.rs:203, commands/src/lib.rs:2795 (skills), and commands/src/lib.rs:2724 (agents) all need the same project_root / HOME bound. Fix shape (~30-50 lines): bound ancestor walk at project root / HOME; add doctor check that surfaces loaded instruction files with paths; add settings.json opt-in toggle for monorepo ancestor inheritance with 'source: ancestor' annotation. Filed in response to Clawhip pinpoint nudge 1494691430096961767 in #clawcode-building-in-public. |
||
|
|
82bd8bbf77 |
ROADMAP #87: fresh-workspace permission default is danger-full-access, doctor silent
Dogfooded 2026-04-17 on main HEAD d6003be against /tmp/cd8. Fresh workspace, no config, no env, no CLI flag: claw status reports 'Permission mode danger-full-access'. 'claw doctor' has no permission-mode check at all -- zero lines mention it. Trace: rusty-claude-cli/src/main.rs:1099-1107 default_permission_mode falls back to PermissionMode::DangerFullAccess when env/config miss. runtime/src/permissions.rs:7-15 PermissionMode ordinal puts DangerFullAccess above WorkspaceWrite/ReadOnly, so current_mode >= required_mode gate at :260-264 auto-approves every tool spec requiring DangerFullAccess or below -- including bash and PowerShell. check_sandbox_health exists at :1895-1910 but no parallel check_permission_health. Status JSON exposes permission_mode but no permission_mode_source field -- fallback indistinguishable from deliberate choice. Interacts badly with #86: corrupt .claw.json silently drops the user's 'plan' choice AND escalates to danger-full-access fallback, and doctor reports Config: ok across both failures. Fix shape (~30-40 lines): add permission doctor check (warn when effective=DangerFullAccess via fallback); add permission_mode_source to status JSON; optionally flip fallback to WorkspaceWrite/Prompt for non-interactive invocations. Filed in response to Clawhip pinpoint nudge 1494683886658257071 in #clawcode-building-in-public. |
||
|
|
d6003be373 |
ROADMAP #86: corrupt .claw.json silently dropped, doctor says config ok
Dogfooded 2026-04-17 on main HEAD 586a92b against /tmp/cd7. A valid .claw.json with permissions.defaultMode=plan applies correctly (claw status shows Permission mode read-only). Corrupt the same file to junk text and: (1) claw status reverts to danger-full-access, (2) claw doctor still reports Config: status=ok, summary='runtime config loaded successfully', with loaded_config_files=0 and discovered_files_count=1 side by side in the same check. Trace: read_optional_json_object at runtime/src/config.rs:674-692 sets is_legacy_config = (file_name == '.claw.json') and on parse failure returns Ok(None) instead of Err(ConfigError::Parse). No warning, no eprintln. ConfigLoader::load() continues past the None, reports overall success. Doctor check at rusty-claude-cli/src/main.rs:1725-1754 emits DiagnosticLevel::Ok whenever load() returned Ok, even with loaded 0/1. Compare a non-legacy settings path at .claw/settings.json with identical corruption: doctor correctly fails loudly. Same file contents, different filename -> opposite diagnostic verdict. Intent was presumably legacy compat with stale historical .claw.json. Implementation now masks live user-written typos. A clawhip preflight that gates on 'status != ok' never sees this. Same surface-lies- about-runtime-truth shape as #80-#84, at the config layer. Fix shape (~20-30 lines): replace silent skip with warn-and-skip carrying the parse error; flip doctor verdict when loaded_count < present_count; expose skipped_files in JSON surface. Filed in response to Clawhip pinpoint nudge 1494676332507041872 in #clawcode-building-in-public. |
||
|
|
586a92ba79 |
ROADMAP #85: unbounded ancestor walk enumerates attacker-placed skills
Dogfooded 2026-04-17 on main HEAD 2eb6e0c. discover_skill_roots at commands/src/lib.rs:2795 iterates cwd.ancestors() unbounded -- no project-root check, no HOME containment, no git boundary. Any .claw/skills, .omc/skills, .agents/skills, .codex/skills, .claude/skills directory on any ancestor path up to / is enumerated and marked active: true in 'claw --output-format json skills'. Repro 1 (cross-tenant skill injection): write /tmp/trap/.agents/skills/rogue/SKILL.md; cd /tmp/trap/inner/work and 'claw skills' shows rogue as active, sourced as Project roots. git init inside the inner CWD does NOT stop the walk. Repro 2 (CWD-dependent skill set): CWD under $HOME yields ~/.agents/skills contents; CWD outside $HOME hides them. Same user, same binary, 26-skill delta driven by CWD alone. Security shape: any attacker-writable ancestor becomes a skill injection primitive. Skill descriptions are free-form Markdown fed into the agent context -- crafted descriptions become prompt injection. tools/src/lib.rs:3295 independently walks ancestors for dispatch, so the injected skill is also executable via slash command, not just listed. Fix shape (~30-50 lines): bound ancestor walk at project root (ConfigLoader::project_root), optionally also at $HOME; require explicit settings.json toggle for monorepo ancestor inheritance; mirror fix in tools/src/lib.rs::push_project_skill_lookup_roots so listed and dispatchable skill surfaces match. Filed in response to Clawhip pinpoint nudge 1494668784382771280 in #clawcode-building-in-public. |
||
|
|
2eb6e0c1ee |
ROADMAP #84: dump-manifests bakes build machine's absolute path into binary
Dogfooded 2026-04-17 on main HEAD 70a0f0c from /tmp/cd4.
'claw dump-manifests' with no arguments emits:
error: Manifest source files are missing.
repo root: /Users/yeongyu/clawd/claw-code
missing: src/commands.ts, src/tools.ts, src/entrypoints/cli.tsx
That path is the *build machine*'s absolute filesystem layout, baked
in via env!('CARGO_MANIFEST_DIR') at rusty-claude-cli/src/main.rs:2016.
strings on the binary reveals the raw path verbatim. JSON surface
(--output-format json) leaks the same path identically.
Three problems: (1) broken default for any user running a distributed
binary because the path won't exist on their machine; (2) privacy
leak -- build user's $HOME segment embedded in the binary and
surfaced to every recipient; (3) reproducibility violation -- two
binaries built from the same commit on different machines produce
different runtime behavior. Same compile-time-vs-runtime family as
ROADMAP #83 (build date injected as 'today').
Fix shape (<=20 lines): drop env!('CARGO_MANIFEST_DIR') from the
runtime default, require CLAUDE_CODE_UPSTREAM / --manifests-dir /
settings entry, reword error to name the required config instead of
leaking a path the user never asked for. Optional polish: add a
settings.json [upstream] entry.
Acceptance: strings <binary> | grep '^/Users/' returns empty for the
shipped binary. Default error surface contains zero absolute paths
from the build machine.
Filed in response to Clawhip pinpoint nudge 1494661235336282248 in
#clawcode-building-in-public.
|
||
|
|
70a0f0cf44 |
ROADMAP #83: DEFAULT_DATE injects build date as 'today' in live system prompt
Dogfooded 2026-04-17 on main HEAD e58c194 against /tmp/cd3. Binary built 2026-04-10; today is 2026-04-17. 'claw system-prompt' emits 'Today's date is 2026-04-10.' The same DEFAULT_DATE constant (rusty-claude-cli/src/main.rs:69-72) is threaded into build_system_prompt() at :6173-6180 and every ClaudeCliSession / StreamingCliSession / non-interactive runner (lines 3649, 3746, 4165, 4211, ...), so the stale date lives in the LIVE agent prompt, not just the system-prompt subcommand. Agents reason from 'today = compile day,' which silently breaks any task that depends on real time (freshness, deadlines, staleness, expiry). Violates ROADMAP principle #4 (branch freshness before blame) and mixes compile-time context into runtime behavior, producing different prompts for two agents on the same main HEAD built a week apart. Fix shape (~30 lines): compute current_date at runtime via chrono::Utc::now().date_naive(), sweep DEFAULT_DATE call sites in main.rs, keep --date override and --version's build-date meaning, add CLAWD_OVERRIDE_DATE env escape for reproducible tests. Filed in response to Clawhip pinpoint nudge 1494653681222811751 in #clawcode-building-in-public. |
||
|
|
e58c1947c1 |
ROADMAP #82: macOS sandbox filesystem_active=true is a lie
Dogfooded 2026-04-17 on main HEAD 1743e60 against /tmp/claw-dogfood-2. claw --output-format json sandbox on macOS reports filesystem_active= true, filesystem_mode=workspace-only but the actual enforcement is only HOME/TMPDIR env-var rebasing at bash.rs:205-209 / :228-232. build_linux_sandbox_command is cfg(target_os=linux)-gated and returns None on macOS, so the fallback path is sh -lc <command> with env tweaks and nothing else. Direct escape proof: a child with HOME=/ws/.sandbox-home TMPDIR=/ws/.sandbox-tmp writes /tmp/claw-escape-proof.txt and mkdir /tmp/claw-probe-target without error. Clawability problem: claws/orchestrators read SandboxStatus JSON and branch on filesystem_active && filesystem_mode=='workspace-only' to decide whether a worker can safely touch /tmp or $HOME. Today that branch lies on macOS. Fix shape option A (low-risk, ~15 lines): compute filesystem_active only where an enforcement path exists, so macOS reports false by default and fallback_reason surfaces the real story. Option B: wire a Seatbelt (sandbox-exec) profile for actual macOS enforcement. Filed in response to Clawhip pinpoint nudge 1494646135317598239 in #clawcode-building-in-public. |
||
|
|
1743e600e1 |
ROADMAP #81: claw status Project root lies about session scope
Dogfooded 2026-04-17 on main HEAD a48575f inside claw-code itself and reproduced on /tmp/claw-split-17. SessionStore::from_cwd at session_control.rs:32-40 uses the raw CWD as input to workspace_fingerprint() (line 295-303), not the project root surfaced in claw status. Result: two CWDs in the same git repo (e.g. ~/clawd/claw-code vs ~/clawd/claw-code/rust) report the same Project root in status but land in two disjoint .claw/sessions/ <fp>/ partitions. claw --resume latest from one CWD returns 'no managed sessions found' even though the adjacent CWD has a live session visible via /session list. Status-layer truth (Project root) and session-layer truth (fingerprint-of-CWD) disagree and neither surface exposes the disagreement -- classic split-truth per ROADMAP pain point #2. Fix shape (<=40 lines): (a) fingerprint the project root instead of raw CWD, or (b) surface partition key explicitly in status. Filed in response to Clawhip pinpoint nudge 1494638583481372833 in #clawcode-building-in-public. |
||
|
|
a48575fd83 |
ROADMAP #80: session-lookup error copy lies about on-disk layout
Dogfooded 2026-04-17 on main HEAD 688295e against /tmp/claw-d4. SessionStore::from_cwd at session_control.rs:32-40 places sessions under .claw/sessions/<workspace_fingerprint>/ (16-char FNV-1a hex at line 295-303), but format_no_managed_sessions and format_missing_session_reference at line 516-526 advertise plain .claw/sessions/ with no fingerprint context. Concrete repro: fresh workspace, no sessions yet, .claw/sessions/ contains foo/ (hash dir, empty) + ffffffffffffffff/foreign.jsonl (foreign workspace session). 'claw --resume latest' still says 'no managed sessions found in .claw/sessions/' even though that directory is not empty -- the sessions just belong to other workspace partitions. Fix shape is ~30 lines: plumb the resolved sessions_root/workspace into the two format helpers, optionally enumerate sibling partitions so error copy tells the operator where sessions from other workspaces are and why they're invisible. Filed in response to Clawhip pinpoint nudge 1494615932222439456 in #clawcode-building-in-public. |
||
|
|
688295ea6c |
ROADMAP #79: claw --output-format json init discards structured InitReport
Dogfooded 2026-04-17 on main HEAD 9deaa29. init.rs:38-113 already
builds a fully-typed InitReport { project_root, artifacts: Vec<
InitArtifact { name, status: InitStatus }> } but main.rs:5436-5454
calls .render() on it and throws the structure away, emitting only
{kind, message: '<prose>'} via init_json_value(). Downstream claws
have to regex 'created|updated|skipped' out of the message string
to know per-artifact state.
version/system-prompt/acp/bootstrap-plan all emit structured payloads
on the same binary -- init is the sole odd-one-out. Fix shape is ~20
lines: add InitReport::to_json_value + InitStatus::as_str, switch
run_init to hold the report instead of .render()-ing it eagerly,
preserve message for backward compat, add output_format_contract
regression.
Filed in response to Clawhip pinpoint nudge 1494608389068558386 in
#clawcode-building-in-public.
|
||
|
|
9deaa29710 |
ROADMAP #78: claw plugins CLI route is a dead constructor
Dogfooded 2026-04-17 on main HEAD d05c868. CliAction::Plugins variant is declared at main.rs:303-307 and wired to LiveCli::print_plugins at main.rs:202-206, but parse_args has no "plugins" arm, so claw plugins / claw plugins list / claw --output-format json plugins all fall through to the LLM-prompt catch-all and emit a missing Anthropic credentials error. This is the sole documented-shaped subcommand that does NOT resolve to a local CLI route: agents, mcp, skills, acp, init, dump-manifests, bootstrap-plan, system-prompt, export all work. grep confirms CliAction::Plugins has exactly one hit in crates/ (the handler), not a constructor anywhere. Filed with a ~15 line parser fix shape plus help/test wiring, matching the pattern already used by agents/mcp/skills. Filed in response to Clawhip pinpoint nudge 1494600832652546151 in #clawcode-building-in-public. |
||
|
|
d05c8686b8 |
ROADMAP #77: typed error-kind contract for --output-format json errors
Dogfooded 2026-04-17 against main HEAD 00d0eb6. Five distinct failure
classes (missing credentials, missing manifests, missing worker state,
session not found, CLI parse) all emit the same {type,error} envelope
with no machine-readable kind/code, so downstream claws have to regex
the prose to route failures. Success payloads already carry a stable
'kind' discriminator; error payloads do not. Fix shape proposes an
ErrorKind discriminant plus hint/context fields to match the success
side contract.
Filed in response to Clawhip pinpoint nudge 1494593284180414484 in
#clawcode-building-in-public.
|
||
|
|
00d0eb61d4 |
US-024: Add token limit metadata for kimi models
Add ModelTokenLimit entries for kimi-k2.5 and kimi-k1.5 to enable preflight context window validation. Per Moonshot AI documentation: - Context window: 256,000 tokens - Max output: 16,384 tokens Includes 3 unit tests: - returns_context_window_metadata_for_kimi_models - kimi_alias_resolves_to_kimi_k25_token_limits - preflight_blocks_oversized_requests_for_kimi_models All tests pass, clippy clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
8d8e2c3afd |
Mark prd.json status as completed
All 23 stories (US-001 through US-023) are now complete. Updated status from "in_progress" to "completed". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
d037f9faa8 |
Fix strip_routing_prefix to handle kimi provider prefix (US-023)
Add "kimi" to the strip_routing_prefix matches so that models like "kimi/kimi-k2.5" have their prefix stripped before sending to the DashScope API (consistent with qwen/openai/xai/grok handling). Also add unit test strip_routing_prefix_strips_kimi_provider_prefix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
330dc28fc2 |
Mark US-023 as complete in prd.json
- Move US-023 from inProgressStories to completedStories - All acceptance criteria met and verified Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
cec8d17ca8 |
Implement US-023: Add automatic routing for kimi models to DashScope
Changes in rust/crates/api/src/providers/mod.rs: - Add 'kimi' alias to MODEL_REGISTRY resolving to 'kimi-k2.5' with DashScope config - Add kimi/kimi- prefix routing to DashScope endpoint in metadata_for_model() - Add resolve_model_alias() handling for kimi -> kimi-k2.5 - Add unit tests: kimi_prefix_routes_to_dashscope, kimi_alias_resolves_to_kimi_k2_5 Users can now use: - --model kimi (resolves to kimi-k2.5) - --model kimi-k2.5 (auto-routes to DashScope) - --model kimi/kimi-k2.5 (explicit provider prefix) All 127 tests pass, clippy clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
4cb1db9faa |
Implement US-022: Enhanced error context for API failures
Add structured error context to API failures: - Request ID tracking across retries with full context in error messages - Provider-specific error code mapping with actionable suggestions - Suggested user actions for common error types (401, 403, 413, 429, 500, 502-504) - Added suggested_action field to ApiError::Api variant - Updated enrich_bearer_auth_error to preserve suggested_action Files changed: - rust/crates/api/src/error.rs: Add suggested_action field, update Display - rust/crates/api/src/providers/openai_compat.rs: Add suggested_action_for_status() - rust/crates/api/src/providers/anthropic.rs: Update error handling All tests pass, clippy clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
5e65b33042 | US-021: Add request body size pre-flight check for OpenAI-compatible provider | ||
|
|
87b982ece5 |
US-011: Performance optimization for API request serialization
Added criterion benchmarks and optimized flatten_tool_result_content: - Added criterion dev-dependency and request_building benchmark suite - Optimized flatten_tool_result_content to pre-allocate capacity and avoid intermediate Vec construction (was collecting to Vec then joining) - Made key functions public for benchmarking: translate_message, build_chat_completion_request, flatten_tool_result_content, is_reasoning_model, model_rejects_is_error_field Benchmark results: - flatten_tool_result_content/single_text: ~17ns - translate_message/text_only: ~200ns - build_chat_completion_request/10 messages: ~16.4µs - is_reasoning_model detection: ~26-42ns All 119 unit tests and 29 integration tests pass. cargo clippy passes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |