Skip to content

Commit a9abaae

Browse files
samuellawerentzclaude
authored andcommitted
fix(plugins): address Codex review on user-level settings fallback
- User-level pass reads only ~/.claude/settings.json (no user-level settings.local.json — it isn't a real Claude Code source), so a stale global local file can't silently reroute the brief/checkpoint. - Resolve project settings from the nearest .claude dir at or above cwd, so a repo-root mapping wins when Claude starts in a subdirectory. - bm-remember / bm-share / bm-status skills now read the same precedence (user-level base, project overrides), matching the README claim that one user-level block covers every command. - README: drop user-level settings.local.json from the precedence note and document the ancestor-walk behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Samuel Lawrentz <samuel.lawerence@plivo.com>
1 parent 445ddb2 commit a9abaae

6 files changed

Lines changed: 100 additions & 44 deletions

File tree

plugins/claude-code/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,15 @@ your project's `.claude/settings.json`. Copy
8080
}
8181
```
8282

83-
The block can also live in your **user-level** `~/.claude/settings.json` (or
84-
`settings.local.json`) — one block there covers every project, no per-repo setup.
85-
Precedence, lowest to highest: user-level `settings.json` → user-level
86-
`settings.local.json` → project `settings.json` → project `settings.local.json`,
87-
merged per key — so a project that pins its own `primaryProject` wins over the
88-
user-level default.
83+
The block can also live in your **user-level** `~/.claude/settings.json` — one
84+
block there covers every project, no per-repo setup. Precedence, lowest to
85+
highest: user-level `settings.json` → project `settings.json` → project
86+
`settings.local.json`, merged per key — so a project that pins its own
87+
`primaryProject` wins over the user-level default. (This mirrors Claude Code's
88+
own sources: `settings.local.json` is project-scoped only, so there is no
89+
user-level `settings.local.json`.) The hooks resolve the project settings from
90+
the nearest `.claude` directory at or above the working directory, so a mapping
91+
in the repo root still applies when Claude starts in a subdirectory.
8992

9093
To enable the capture reflexes, also set `"outputStyle": "basic-memory"` in your
9194
settings (or select it via `/config`).

plugins/claude-code/hooks/pre-compact.sh

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,47 @@ transcript_path = payload.get("transcript_path") or ""
6464
session_id = payload.get("session_id") or ""
6565
6666
67+
def _read_block(path):
68+
try:
69+
with open(path) as fh:
70+
block = json.load(fh).get("basicMemory")
71+
except FileNotFoundError:
72+
return None
73+
except Exception:
74+
return None
75+
return block if isinstance(block, dict) else None
76+
77+
78+
def _project_dir(directory):
79+
# Nearest ancestor (including directory) holding a .claude settings file.
80+
d = os.path.abspath(directory)
81+
while True:
82+
for name in ("settings.json", "settings.local.json"):
83+
if os.path.isfile(os.path.join(d, ".claude", name)):
84+
return d
85+
parent = os.path.dirname(d)
86+
if parent == d:
87+
return os.path.abspath(directory)
88+
d = parent
89+
90+
6791
def load_settings(directory):
68-
# Same precedence as session-start.sh: user-level ~/.claude is the base,
69-
# the project's .claude overrides it, settings.local.json wins per level.
92+
# Same precedence as session-start.sh: user-level ~/.claude/settings.json is
93+
# the base (no user-level settings.local.json — it isn't a real Claude Code
94+
# source), then the nearest project .claude (settings.json, then
95+
# settings.local.json) overrides it. cwd may be a repo subdirectory, so walk
96+
# ancestors to the project root rather than reading cwd alone.
7097
merged = {}
7198
home = os.path.expanduser("~")
72-
dirs = [home] if os.path.abspath(directory) == home else [home, directory]
73-
for d in dirs:
74-
for name in ("settings.json", "settings.local.json"):
75-
path = os.path.join(d, ".claude", name)
76-
try:
77-
with open(path) as fh:
78-
merged.update(json.load(fh).get("basicMemory") or {})
79-
except FileNotFoundError:
80-
continue
81-
except Exception:
82-
continue
99+
sources = [(home, ("settings.json",))]
100+
project = _project_dir(directory)
101+
if os.path.abspath(project) != home:
102+
sources.append((project, ("settings.json", "settings.local.json")))
103+
for d, names in sources:
104+
for name in names:
105+
block = _read_block(os.path.join(d, ".claude", name))
106+
if block is not None:
107+
merged.update(block)
83108
return merged
84109
85110

plugins/claude-code/hooks/session-start.sh

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,30 +76,52 @@ cwd = payload.get("cwd") or os.getcwd()
7676
7777
7878
# --- Load plugin config from .claude settings (local overrides committed) ---
79-
# Precedence (lowest to highest): user-level ~/.claude/settings.json,
80-
# ~/.claude/settings.local.json, then the project's .claude/settings.json and
81-
# .claude/settings.local.json. A single user-level basicMemory block can cover
82-
# every project without running setup per repo; any project can still pin its
83-
# own mapping, which wins. `found` is True if any file declared a basicMemory
84-
# block at all — its presence is the first-run sentinel (setup writing it stops
85-
# the nudge below).
79+
# Precedence (lowest to highest): the user-level ~/.claude/settings.json, then
80+
# the project's .claude/settings.json and .claude/settings.local.json. A single
81+
# user-level basicMemory block can cover every project without running setup per
82+
# repo; any project can still pin its own mapping, which wins. We mirror Claude
83+
# Code's real sources: user level is settings.json only (there is no user-level
84+
# settings.local.json), local settings are project-scoped. Because the hook cwd
85+
# can be a repo subdirectory, we walk ancestors to the nearest .claude config so
86+
# a project-root mapping is honoured instead of skipped. `found` is True if any
87+
# file declared a basicMemory block — its presence is the first-run sentinel
88+
# (setup writing it stops the nudge below).
89+
def _read_block(path):
90+
try:
91+
with open(path) as fh:
92+
block = json.load(fh).get("basicMemory")
93+
except FileNotFoundError:
94+
return None
95+
except Exception:
96+
return None
97+
return block if isinstance(block, dict) else None
98+
99+
100+
def _project_dir(directory):
101+
# Nearest ancestor (including directory) holding a .claude settings file.
102+
d = os.path.abspath(directory)
103+
while True:
104+
for name in ("settings.json", "settings.local.json"):
105+
if os.path.isfile(os.path.join(d, ".claude", name)):
106+
return d
107+
parent = os.path.dirname(d)
108+
if parent == d:
109+
return os.path.abspath(directory)
110+
d = parent
111+
112+
86113
def load_settings(directory):
87114
merged = {}
88115
found = False
89116
home = os.path.expanduser("~")
90-
dirs = [home] if os.path.abspath(directory) == home else [home, directory]
91-
for d in dirs:
92-
for name in ("settings.json", "settings.local.json"):
93-
path = os.path.join(d, ".claude", name)
94-
try:
95-
with open(path) as fh:
96-
data = json.load(fh)
97-
except FileNotFoundError:
98-
continue
99-
except Exception:
100-
continue
101-
block = data.get("basicMemory")
102-
if isinstance(block, dict):
117+
sources = [(home, ("settings.json",))]
118+
project = _project_dir(directory)
119+
if os.path.abspath(project) != home:
120+
sources.append((project, ("settings.json", "settings.local.json")))
121+
for d, names in sources:
122+
for name in names:
123+
block = _read_block(os.path.join(d, ".claude", name))
124+
if block is not None:
103125
found = True
104126
merged.update(block)
105127
return merged, found

plugins/claude-code/skills/bm-remember/SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ Capture `$ARGUMENTS` into Basic Memory as a quick note, keeping the user's words
1010

1111
## Steps
1212

13-
1. **Resolve config.** Read `.claude/settings.json` (and `.claude/settings.local.json`
14-
if it exists) and look for the `basicMemory` block:
13+
1. **Resolve config.** Read the `basicMemory` block with the same precedence the
14+
hooks use: user-level `~/.claude/settings.json` as the base, then the project's
15+
`.claude/settings.json` and `.claude/settings.local.json` override it per key. A
16+
user-level block alone is enough; a project can still pin its own values:
1517
- `rememberFolder` — folder for quick captures (default: `bm-remember`)
1618
- `primaryProject` — project to write to (default: omit the `project` argument so
1719
Basic Memory uses its default project)

plugins/claude-code/skills/bm-share/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ project — session checkpoints and `/basic-memory:bm-remember` always stay pers
1212

1313
## Steps
1414

15-
1. **Resolve config.** Read `.claude/settings.json` (+ `.local`) `basicMemory`:
15+
1. **Resolve config.** Read the `basicMemory` block with the hooks' precedence:
16+
user-level `~/.claude/settings.json` as the base, then the project's
17+
`.claude/settings.json` and `.claude/settings.local.json` override per key:
1618
- `teamProjects` — a map of `<project-ref>``{ "promoteFolder": "shared" }`.
1719
These are the allowed share targets. `<project-ref>` is a workspace-qualified
1820
name (e.g. `my-team-2/notes`) or an `external_id` UUID.

plugins/claude-code/skills/bm-status/SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ This is a quick diagnostic — gather the facts and lay them out; don't over-inv
1515
neither is found, report that Basic Memory isn't installed or on PATH, and stop —
1616
nothing else will work without it.
1717

18-
2. **Configuration.** Read `.claude/settings.json` (and `.claude/settings.local.json`
19-
if present) and report:
18+
2. **Configuration.** Read the `basicMemory` block with the hooks' precedence —
19+
user-level `~/.claude/settings.json` as the base, then the project's
20+
`.claude/settings.json` and `.claude/settings.local.json` overriding per key —
21+
and report (note when a value comes from the user-level block vs. the project):
2022
- From the `basicMemory` block: `primaryProject` (or note none is pinned — the
2123
default project is used), `secondaryProjects` (team/shared read sources),
2224
`teamProjects` (share targets for `/basic-memory:bm-share`), `captureFolder`

0 commit comments

Comments
 (0)