mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-26 23:04:58 +08:00
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.
This commit is contained in:
84
ROADMAP.md
84
ROADMAP.md
@@ -3094,3 +3094,87 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
|
||||
**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`.
|
||||
|
||||
**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.
|
||||
|
||||
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."
|
||||
|
||||
**Concrete repro.**
|
||||
```
|
||||
$ mkdir -p /tmp/cdGG/nested/deep/dir
|
||||
$ cd /tmp/cdGG && git init -q .
|
||||
$ echo '{"model":"haiku","permissions":{"defaultMode":"read-only"}}' > /tmp/cdGG/.claw.json
|
||||
|
||||
$ cd /tmp/cdGG/nested/deep/dir
|
||||
$ claw --output-format json status | jq '{permission_mode, workspace: {cwd, project_root}}'
|
||||
{
|
||||
"permission_mode": "danger-full-access",
|
||||
"workspace": {
|
||||
"cwd": "/private/tmp/cdGG/nested/deep/dir",
|
||||
"project_root": "/private/tmp/cdGG"
|
||||
}
|
||||
}
|
||||
# project_root correctly walks UP to /tmp/cdGG. But permission_mode is danger-full-access
|
||||
# (the compile-time fallback) instead of read-only (what .claw.json says).
|
||||
|
||||
$ claw --output-format json doctor 2>/dev/null | jq '.checks[] | select(.name=="config") | {status, summary, details}'
|
||||
{
|
||||
"status": "ok",
|
||||
"summary": "no config files present; defaults are active",
|
||||
"details": [
|
||||
"Config files loaded 0/0",
|
||||
"MCP servers 0",
|
||||
"Discovered files <none> (defaults active)"
|
||||
]
|
||||
}
|
||||
# Zero files discovered. .claw.json at /tmp/cdGG/.claw.json is invisible.
|
||||
# "defaults are active" — but the operator's intent was read-only.
|
||||
|
||||
# Compare: CLAUDE.md discovery DOES walk ancestors (per #85)
|
||||
$ echo '# Instructions' > /tmp/cdGG/CLAUDE.md
|
||||
$ claw --output-format json status | jq '.workspace.memory_file_count'
|
||||
1
|
||||
# CLAUDE.md found via ancestor walk. .claw.json wasn't.
|
||||
|
||||
# Also compare: running from the repo root works as expected
|
||||
$ cd /tmp/cdGG && claw --output-format json status | jq '.permission_mode'
|
||||
"read-only"
|
||||
# From cwd=repo-root, .claw.json at cwd IS discovered. Config works.
|
||||
# Same operator, same workspace, different cwd → different config loaded.
|
||||
```
|
||||
|
||||
**Trace path.**
|
||||
- `rust/crates/runtime/src/config.rs:242-270` — `ConfigLoader::discover`:
|
||||
```rust
|
||||
vec![
|
||||
ConfigEntry { source: User, path: user_legacy_path },
|
||||
ConfigEntry { source: User, path: self.config_home.join("settings.json") },
|
||||
ConfigEntry { source: Project, path: self.cwd.join(".claw.json") },
|
||||
ConfigEntry { source: Project, path: self.cwd.join(".claw").join("settings.json") },
|
||||
ConfigEntry { source: Local, path: self.cwd.join(".claw").join("settings.local.json") },
|
||||
]
|
||||
```
|
||||
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.
|
||||
- `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.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
**Why this is specifically a clawability gap.**
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
**Fix shape — anchor config discovery at `project_root` with cwd overlay.**
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
Reference in New Issue
Block a user