This spec describes how knobs.cc determines a user's effective Claude Code settings and renders them in the UI alongside their provenance ("where this value was set"). v1 is read-only; nothing in this spec writes back.
The spec is a phased plan. Phase 1 is the only one being implemented now; later phases describe scope, not commitments.
Status of open phases lives in
roadmap.md. The phase sections below stay as design references; what's shipped vs. pending is tracked there.
- For every setting Claude Code might read, show the effective value the user's session would see, plus the layer that produced it.
- Make "which layer wins, and why" debuggable without reading docs — the
precedence chain in
inventory.md:55should be legible from the UI. - Stay inside the Tauri 2 read-only boundary: explicit
#[tauri::command]reads, no fs/shell/dialog plugin permissions, no write commands.
- Editing settings.
- Reading settings of an active
claudesession (CLI flags passed to a running process, in particular). - Reasoning about plugin/skill/agent frontmatter as a settings source.
Per spec/inventory.md:55, plus env vars folded in:
| # | Layer | Source | Phase |
|---|---|---|---|
| 1 | managed |
Server-managed > MDM (plist/registry) > file-based > HKCU | |
| 2 | cli |
Attached claude's argv, parsed against catalog/cli-settings-map.json (attach-mode.md) |
|
| 3 | env |
Process env. Reads the attached claude's environ when attached; falls back to knobs.cc's own process env otherwise. The EnvVarsPanel surfaces both side-by-side. | |
| 4 | project_local |
<project>/.claude/settings.local.json — <project> resolves to the attached claude's cwd or a user-picked directory (attach-mode.md) |
|
| 5 | project |
<project>/.claude/settings.json — same grounding as project_local |
|
| 6 | user |
~/.claude/settings.json |
|
| 7 | default |
Claude Code's compiled-in defaults (catalog-derived) |
When no claude is attached, the cli row is empty and the rail greys
it; when no claude is attached and no project directory has been
picked, project / project_local also grey out (falling back to
reading from knobs.cc's own CWD only to satisfy tests).
- Scalars and objects: last-wins by precedence (highest layer present).
- Array-merged fields: concatenated and de-duplicated across all layers,
not replaced. Inventory calls this out in
inventory.md:58. Known array-merged fields includepermissions.allow,permissions.ask,permissions.deny,permissions.additionalDirectories,sandbox.filesystem.allowWrite, and similar list-shaped permission fields. For these, each element carries its own provenance; the field as a whole has no single source. - Managed-tier internal merge: within the managed tier, only one source
applies (
inventory.md:50); but file-based managed tier mergesmanaged-settings.jsonwithmanaged-settings.d/*.jsonalphabetically. This is collapsed into a singlemanagedlayer at the public API boundary.
A single Tauri command returns one snapshot. The shape is stable across phases — later phases populate more fields but don't reshape existing ones.
type LayerSource =
| "managed" | "cli" | "env"
| "project_local" | "project" | "user"
| "default";
interface LayerRead {
source: LayerSource;
// Absolute path that was read. Null for env/cli/default.
path: string | null;
// "ok" if parsed; "missing" if file absent; "error" if present but unreadable.
status: "ok" | "missing" | "error";
// Parsed JSON, or null if missing/error.
raw: unknown | null;
// Set when status === "error".
error: string | null;
}
interface ProvenancedValue {
// The effective value at this leaf.
value: unknown;
// Single source for scalars/objects. For array-merged fields, null at the
// field level; each element has its own source in `elements`.
source: LayerSource | null;
// Only set for array-merged fields.
elements?: { value: unknown; source: LayerSource }[];
}
interface SettingsSnapshot {
// Each layer that was attempted, in precedence order (highest first).
layers: LayerRead[];
// Effective tree, leaves replaced with ProvenancedValue.
effective: Record<string, unknown>;
// Project root used for resolution; null if not in a project.
project_root: string | null;
// Diagnostics not tied to a single layer (e.g., HOME unresolvable).
diagnostics: { level: "warn" | "error"; message: string }[];
// Sibling read of `managed-mcp.json` (Phase 2). Not part of the
// precedence merge; surfaced so the UI can indicate admin-shipped
// MCP policy. Same LayerRead shape as a precedence layer.
managed_mcp: LayerRead;
}Scope: the three file-based layers a user always has on their own machine.
- Backend: new
settingsmodule insrc-tauri/src/. Tauri commandread_settings_layersregistered alongsidegreet. - Read these files in this precedence order:
<cwd>/.claude/settings.local.json(project_local)<cwd>/.claude/settings.json(project)~/.claude/settings.json(user)
- Project root: post-pivot, resolved per
attach-mode.md— the attached claude process's cwd, a user-picked directory, or (legacy fallback only)std::env::current_dir(). Theread_settings_layerscommand grows two optional args (attached_pid,project_root_override); calling with neither preserves the pre-pivot CWD behavior for tests and the no-attach-no-picker default. Closed #12. - Each layer reports
ok/missing/errorindependently — a malformed user file does not block reading the project file. effectiveis computed last-wins. Array-merge semantics are deferred to Phase 3; for Phase 1, arrays follow the same last-wins rule and we accept that this is wrong forpermissions.*. The spec records the limitation so it isn't forgotten.ProvenancedValueis emitted for every leaf, withelementsalways unset.- Frontend: replace the boilerplate App with a minimal view that invokes
read_settings_layerson mount and renders the snapshot as syntax-shaped JSON. No styling work yet — this is a vertical-slice smoke test that the IPC, types, and path resolution all line up.
Out of phase 1: managed sources, env vars, array merge, project-root discovery, catalog cross-reference, list/badge UI, refresh, file watcher.
- Resolve managed paths per OS (
inventory.md:46–48). - Merge
managed-settings.d/*.jsonalphabetically intomanaged-settings.json. - Add a
managedlayer to the snapshot (file-tier only; plist/registry in Phase 6). - Surface
managed-mcp.jsonas a sibling read (it's not part of the merge but the UI should know it exists).
- New module reads the env-var subset documented in
inventory.md§3 (auth, endpoints, model selection, feature toggles, timeouts, shell/tooling). - Folded into the snapshot as the
envlayer. Env vars don't share a JSON shape with settings.json, so the merge is per-knob: each catalog entry records which env var (if any) maps to it. - Implement array-concat-dedup for the known array-merged fields. Populate
elementsfor those fields, setsource: nullat the field level.
The
envsettings-precedence layer (described above) projects 8 mapped OS env vars onto settings keys viacatalog/env-settings-map.json— that's its only job. The orthogonal "what env vars from the full 220-entry catalog is Claude Code seeing right now?" surface is the EnvVarsPanel (seeinspector-ui.md§ "Sibling surfaces"), which reads the user's process env via theread_shell_env_varsTauri command and joins it with settings.json'senvblock per layer. The two surfaces don't overlap: inspectorenv.*rows are filtered out entirely; the panel owns that question end-to-end.
UI direction is locked: a three-pane DevTools-style Inspector
(precedence rail / settings list / detail drawer). Layout, design
primitives (presence indicator, source badges, layer waterfall), empty-
state copy, and keyboard model are specified in
inspector-ui.md. Visual reference:
mocks/01-inspector.html.
Phase 4 work items:
- Replace the JSON dump with the Inspector layout.
- Flat searchable/sortable settings list (driven by the catalog; unset entries shown greyed and hidden by default).
- Implement source badges and the presence indicator (the two reusable components that appear across the rail, list, and drawer).
- Project-root discovery: walk up from cwd to find the nearest
.claude/, with a manual override.
- Click a row → drawer showing every layer's contribution to that key, with the winning layer highlighted.
- For array-merged fields, render per-element provenance.
- Pull description, type, default, deprecation, and
verifyflag fromcatalog.json(when the catalog-sync harness produces one) or from a hand-extracted subset ofinventory.mduntil then. - For
env.<VAR>rows, cross-reference the env-vars catalog by name and surface the env-var'spurposeprose instead of the generic parent-envdescription. (Shipped 2026-05-07 — the first drawer-side consumer of a non-settings catalog, validating the multi-catalog cross-reference seam.) - "Default" layer becomes meaningful here — values not set anywhere fall
through to the catalog-declared default and are tagged
default.
- macOS: read
com.anthropic.claudecodemanaged-preferences plist. Shipped 2026-05-06.read_managed_layerchecks/Library/Managed Preferences/<user>/com.anthropic.claudecode.plist(per-user MDM) and/Library/Managed Preferences/com.anthropic.claudecode.plist(system MDM) in that order — same ordercfprefsdresolves managed defaults. A present plist shadows the file-based source perinventory.md:50; if the plist exists but is malformed, the layer reportserrorrather than silently falling back to file-based — the admin should see their broken policy, not have a different source invisibly take over. Plist values are converted fromplist::Value→serde_json::Value(Date/Data are dropped — they have no clean JSON shape and shouldn't appear in claude-code policy). Theplistcrate is a macOS-only[target.'cfg(target_os = "macos")']dependency, so Linux and Windows builds don't pull it in. Watcher + opener-scope updated for the new paths. - Windows: read
HKLM\SOFTWARE\Policies\ClaudeCodeandHKCU\SOFTWARE\Policies\ClaudeCode. Shipped 2026-05-06. Each hive'sSettingsvalue is aREG_SZcontaining a JSON-encoded settings document (inventory.md:48). The reader uses thewinregcrate as a Windows-only[target.'cfg(target_os = "windows")']dep so other targets don't pull it in. Per-tier order on Windows is HKLM > file-based > HKCU — HKCU is intentionally the lowest-priority managed source perinventory.md:50, because per-user policy is meant to be admin-overridable by file-based, which is in turn overridden by HKLM/MDM. Same "present-but-malformed surfaces error rather than silently falling through" rule as the macOS plist case — admins should see their broken policy. Display path on the rail/drawer isHIVE\SOFTWARE\Policies\ClaudeCode\Settingsso the value name is unambiguous. The frontend'sWaterfallRow.PathNotedetects registry-style paths (isRegistryPath) and renders them as a non-clickable label — the opener plugin can't do anything with a registry path, and dangling a button that silently no-ops is worse UX than rendering plain text. - Apply managed-tier precedence (
inventory.md:50) to pick the single managed source that wins.
- File watcher (via Rust, not the fs plugin) for live updates when settings files change on disk.
- Surface malformed-JSON / permission-denied / missing-HOME diagnostics in the UI.
- Empty states for each layer slot, including the unreachable-by-design
clislot.
- Resolving CLI flags or env of a running
claudeprocess. Both gaps (the emptycliprecedence slot, and the EnvVarsPanel showing knobs.cc's own env rather than claude's) are tracked together as the runtime-introspection feature at #11. - Grounding
project/project_localresolution in a user-chosen claude session rather than knobs.cc's CWD. Tracked at #12; shares process-discovery plumbing with #11 but is also addressable on its own via a path picker. - Plugin/skill/agent frontmatter as a settings source.
- Any write path.