Skip to content

Commit b2366d1

Browse files
committed
ROADMAP ultraworkers#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 ultraworkers#85 over-discovery). Same subsystem category, opposite policy, no doc. Security-adjacent per ultraworkers#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 ultraworkers#106 + ultraworkers#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: ultraworkers#85: skills ancestor walk UNBOUNDED (over-discovery) ultraworkers#88: CLAUDE.md ancestor walk enables injection ultraworkers#110: config NO ancestor walk (under-discovery) Natural bundle: ultraworkers#85 + ultraworkers#110 (ancestor policy unification), or ultraworkers#85 + ultraworkers#88 + ultraworkers#110 (full three-way ancestor-walk audit). Filed in response to Clawhip pinpoint nudge 1494865079567519834 in #clawcode-building-in-public.
1 parent 16244ce commit b2366d1

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

ROADMAP.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3094,3 +3094,87 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
30943094
**Blocker.** None. All additive; no breaking changes. `ValidationResult` already carries the data — this is pure plumbing from validator → loader → config type → doctor/status surface. Parallel to #107's proposed plumbing for `HookProgressEvent`.
30953095
30963096
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdDD` on main HEAD `21b2773` in response to Clawhip pinpoint nudge at `1494857528335532174`. Joins **truth-audit / diagnostic-integrity** (#80–#87, #89, #100, #102, #103, #105, #107) — doctor says "ok" while the validator flagged deprecations. Joins **unplumbed-subsystem** (#78, #96, #100, #102, #103, #107) — structured validator output JSON-invisible. Joins **Claude Code migration parity** (#103) — legacy claude-code-style `permissionMode` at top level is deprecated but the migration path is stderr-only. Natural bundle: **#100 + #102 + #103 + #107 + #109** — five-way doctor-surface-coverage plus structured-warnings (becomes the "doctor stops lying" PR). Also **#107 + #109** — stderr-only-prose-warning sweep (hook progress events + config warnings), same plumbing pattern, paired tiny fix. Session tally: ROADMAP #109.
3097+
3098+
110. **`ConfigLoader::discover` only looks at `$CWD/.claw.json`, `$CWD/.claw/settings.json`, and `$CWD/.claw/settings.local.json` — it does not walk up to `project_root` (the detected git root) to find config. A developer with `.claw.json` at the repo root who runs claw from a subdirectory gets ZERO config loaded. `doctor` reports `config: ok, no config files present; defaults are active`. `status.permission_mode` resolves to `danger-full-access` (the compile-time fallback) silently. Meanwhile CLAUDE.md / instruction files DO walk ancestors unbounded (per #85). Two adjacent discovery mechanisms, opposite strategies, no documentation, silently inconsistent behavior** — dogfooded 2026-04-18 on main HEAD `16244ce` from `/tmp/cdGG/nested/deep/dir`. The workspace-check correctly identifies `project_root: /tmp/cdGG` (via git-root walk), but config discovery never reaches that directory. A `.claw.json` at `/tmp/cdGG/.claw.json` (the project root) is INVISIBLE from any subdirectory below it. Under-discovery is the opposite failure mode from #85's over-discovery — same meta-issue: "ancestor walk policy is subsystem-by-subsystem ad-hoc, not principled."
3099+
3100+
**Concrete repro.**
3101+
```
3102+
$ mkdir -p /tmp/cdGG/nested/deep/dir
3103+
$ cd /tmp/cdGG && git init -q .
3104+
$ echo '{"model":"haiku","permissions":{"defaultMode":"read-only"}}' > /tmp/cdGG/.claw.json
3105+
3106+
$ cd /tmp/cdGG/nested/deep/dir
3107+
$ claw --output-format json status | jq '{permission_mode, workspace: {cwd, project_root}}'
3108+
{
3109+
"permission_mode": "danger-full-access",
3110+
"workspace": {
3111+
"cwd": "/private/tmp/cdGG/nested/deep/dir",
3112+
"project_root": "/private/tmp/cdGG"
3113+
}
3114+
}
3115+
# project_root correctly walks UP to /tmp/cdGG. But permission_mode is danger-full-access
3116+
# (the compile-time fallback) instead of read-only (what .claw.json says).
3117+
3118+
$ claw --output-format json doctor 2>/dev/null | jq '.checks[] | select(.name=="config") | {status, summary, details}'
3119+
{
3120+
"status": "ok",
3121+
"summary": "no config files present; defaults are active",
3122+
"details": [
3123+
"Config files loaded 0/0",
3124+
"MCP servers 0",
3125+
"Discovered files <none> (defaults active)"
3126+
]
3127+
}
3128+
# Zero files discovered. .claw.json at /tmp/cdGG/.claw.json is invisible.
3129+
# "defaults are active" — but the operator's intent was read-only.
3130+
3131+
# Compare: CLAUDE.md discovery DOES walk ancestors (per #85)
3132+
$ echo '# Instructions' > /tmp/cdGG/CLAUDE.md
3133+
$ claw --output-format json status | jq '.workspace.memory_file_count'
3134+
1
3135+
# CLAUDE.md found via ancestor walk. .claw.json wasn't.
3136+
3137+
# Also compare: running from the repo root works as expected
3138+
$ cd /tmp/cdGG && claw --output-format json status | jq '.permission_mode'
3139+
"read-only"
3140+
# From cwd=repo-root, .claw.json at cwd IS discovered. Config works.
3141+
# Same operator, same workspace, different cwd → different config loaded.
3142+
```
3143+
3144+
**Trace path.**
3145+
- `rust/crates/runtime/src/config.rs:242-270` — `ConfigLoader::discover`:
3146+
```rust
3147+
vec![
3148+
ConfigEntry { source: User, path: user_legacy_path },
3149+
ConfigEntry { source: User, path: self.config_home.join("settings.json") },
3150+
ConfigEntry { source: Project, path: self.cwd.join(".claw.json") },
3151+
ConfigEntry { source: Project, path: self.cwd.join(".claw").join("settings.json") },
3152+
ConfigEntry { source: Local, path: self.cwd.join(".claw").join("settings.local.json") },
3153+
]
3154+
```
3155+
Every project+local entry uses `self.cwd.join(...)`. No ancestor walk. No consultation of `project_root` / git-root. If cwd ≠ project_root, config is lost.
3156+
- `rust/crates/runtime/src/config.rs:292` — `for entry in self.discover()` — iterates the fixed list and attempts to read each. A nonexistent file at cwd is simply treated as absent; the "project" config that actually exists at the git root is never even considered.
3157+
- `rust/crates/runtime/src/prompt.rs:203-224` — `discover_instruction_files` (for CLAUDE.md) does walk ancestors up to filesystem root (#85's over-discovery gap). Same concept, opposite strategy, different subsystem. The two ancestor-discovery policies disagree for no documented reason.
3158+
- `rust/crates/rusty-claude-cli/src/main.rs:1485` — `render_doctor_report` reports `workspace.project_root` correctly via a git-root walk. The same walk is NOT consulted by `ConfigLoader`. Project-root detection and config-discovery are independent code paths with incompatible anchoring.
3159+
3160+
**Why this is specifically a clawability gap.**
3161+
1. *Silent config loss in the common-case layout.* The standard project layout is: `.claw.json` at the git root, multiple subdirectories for code/tests/docs. Developers routinely `cd` into subdirectories to run builds or tests. Claws running inside a worktree subdirectory (e.g., a test runner's cwd at `$REPO/tests`) get `defaults are active` — not the operator's intended config.
3162+
2. *Asymmetry with CLAUDE.md / instruction files.* `#85` flags that instruction-file discovery walks ancestors unbounded (a different problem — over-discovery). Here: config-file discovery does not walk ancestors at all (under-discovery). Same subsystem category (workspace-scoped discovery), opposite behavior. No documentation explains why.
3163+
3. *Asymmetry with project_root detection.* The same `render_doctor_report` / `status` output correctly reports `project_root: /tmp/cdGG` — it knows how to walk up. `ConfigLoader` has access to the same cwd and could call the same helper, but it doesn't. Two adjacent pieces of workspace logic disagree.
3164+
4. *Doctor lies by omission.* `config: ok, no config files present; defaults are active` implies the operator hasn't configured anything. But the operator HAS configured — claw just doesn't see it. "0/0 files present" is misleading when a file DOES exist at the project root.
3165+
5. *Permission-mode fallback silently applies.* Per #87, the compile-time fallback is `danger-full-access`. Combined with this finding: cd'ing to a subdirectory silently upgrades permissions from read-only (configured) to danger-full-access (fallback). Security-adjacent: workspace-location-dependent permission drift.
3166+
6. *Roadmap Product Principle #4 ("Branch freshness before blame")* assumes per-workspace config exists and is honored. Per-workspace config is unreliable when any subdirectory invocation loses it.
3167+
3168+
**Fix shape — anchor config discovery at `project_root` with cwd overlay.**
3169+
1. *Walk ancestors to find the outermost `project_root` marker (git root or `.claw` dir), then discover config from that anchor.* Add a `project_root_for(&cwd)` helper (reuse the existing git-root walker from `render_doctor_report`). Config search order becomes: user → project_root/.claw.json → project_root/.claw/settings.json → cwd/.claw.json (overlay) → cwd/.claw/settings.json (overlay) → cwd/.claw/settings.local.json. ~40 lines.
3170+
2. *Optionally, also walk intermediate ancestors between cwd and project_root.* A `.claw.json` at `/tmp/cdGG/nested/.claw.json` (intermediate) should be discoverable from `/tmp/cdGG/nested/deep/dir`. Symmetric with how git sub-project conventions work and with `.gitignore` precedence. ~15 lines.
3171+
3. *Surface "where did my config come from" in doctor.* Add per-discovered-file source-path + source-directory to the doctor JSON. Operators can see exactly which file contributed each key (pairs with #106's proposed provenance and #109's warnings surface). ~20 lines.
3172+
4. *Detect and warn on ambiguous cwd ≠ project_root cases.* When cwd has no config but project_root does, emit a structured warning `config_scope_mismatch: {cwd, project_root, project_root_config_path}`. ~10 lines. Same plumbing as #109's proposed warnings surface.
3173+
5. *Documentation parity.* Document the ancestor-walk policy for both CLAUDE.md and config files. Ideally, unify them under a single policy (walk to project_root, overlay cwd files). ~5 lines of doc.
3174+
6. *Regression tests.* Per cwd-relative-to-project-root position (at root, 1 level deep, 3 levels deep, outside repo). Overlay precedence test. Config-scope-mismatch warning test.
3175+
3176+
**Acceptance.** `cd /tmp/cdGG/nested/deep/dir && claw --output-format json status` with `.claw.json` at `/tmp/cdGG/.claw.json` exposes `permission_mode: "read-only"` (config honored from project root), not `danger-full-access` (fallback). `doctor` reports `Config files loaded 1/N` with the project-root config file discovered. `cd /tmp/cdGG/nested && echo '{"model":"opus"}' > .claw.json` produces a discoverable overlay. Running from any subdirectory yields deterministic per-workspace config resolution. Documentation explains the policy.
3177+
3178+
**Blocker.** None. `project_root_for` helper trivially reusable from the git-root walker. Discovery list is additive — adding ancestor entries doesn't break existing cwd-anchored configs. Most invasive piece is the architectural decision: walk-to-project-root + cwd-overlay (this proposal), or walk-every-ancestor-like-CLAUDE.md (#85's current over-broad policy), or unify both under a single policy.
3179+
3180+
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdGG/nested/deep/dir` on main HEAD `16244ce` in response to Clawhip pinpoint nudge at `1494865079567519834`. Joins **truth-audit / diagnostic-integrity** (#80–#87, #89, #100, #102, #103, #105, #107, #109) — doctor reports "ok, defaults active" when the operator actually has a config. Joins **discovery-overreach / security-shape** (#85, #88) as the opposite-direction sibling: #85 over-discovers instruction files; #110 under-discovers config files. Cross-cluster with **Reporting-surface / config-hygiene** (#90, #91, #92) — this is the canonical config-discovery policy bug. Natural bundle: **#85 + #110** — unify ancestor-discovery policy across CLAUDE.md + config. Also **#85 + #88 + #110** as the three-way "ancestor-walk policy audit" covering skills over-discovery, CLAUDE.md prompt injection via ancestors, and config under-discovery from subdirectories. Session tally: ROADMAP #110.

0 commit comments

Comments
 (0)