Skip to content

Commit aa454ee

Browse files
samuellawerentzclaude
authored andcommitted
feat(plugins): fall back to user-level ~/.claude settings for hook config
The SessionStart and PreCompact hooks read basicMemory config only from <cwd>/.claude/settings.json, so every project needs its own block (or a bm-setup run) before briefs resolve a pinned project and checkpoints write at all. Users with one primary knowledge graph end up repeating the same config in every repo. Make load_settings in both hooks merge user-level ~/.claude/settings.json and settings.local.json as the base, with the project's .claude files overriding per key — same layering Claude Code itself uses for settings. A single user-level basicMemory block now covers every project, while any project can still pin its own mapping, which wins. Documented in the plugin README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Samuel Lawrentz <samuel.lawerence@plivo.com>
1 parent f5c3c6c commit aa454ee

3 files changed

Lines changed: 44 additions & 25 deletions

File tree

plugins/claude-code/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ 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.
89+
8390
To enable the capture reflexes, also set `"outputStyle": "basic-memory"` in your
8491
settings (or select it via `/config`).
8592

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,21 @@ session_id = payload.get("session_id") or ""
6565
6666
6767
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.
6870
merged = {}
69-
for name in ("settings.json", "settings.local.json"):
70-
path = os.path.join(directory, ".claude", name)
71-
try:
72-
with open(path) as fh:
73-
merged.update(json.load(fh).get("basicMemory") or {})
74-
except FileNotFoundError:
75-
continue
76-
except Exception:
77-
continue
71+
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
7883
return merged
7984
8085

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

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,25 +76,32 @@ cwd = payload.get("cwd") or os.getcwd()
7676
7777
7878
# --- Load plugin config from .claude settings (local overrides committed) ---
79-
# Precedence: settings.local.json (per-user) wins over settings.json (team).
80-
# `found` is True if either file declared a basicMemory block at all — its
81-
# presence is the first-run sentinel (setup writing it stops the nudge below).
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).
8286
def load_settings(directory):
8387
merged = {}
8488
found = False
85-
for name in ("settings.json", "settings.local.json"):
86-
path = os.path.join(directory, ".claude", name)
87-
try:
88-
with open(path) as fh:
89-
data = json.load(fh)
90-
except FileNotFoundError:
91-
continue
92-
except Exception:
93-
continue
94-
block = data.get("basicMemory")
95-
if isinstance(block, dict):
96-
found = True
97-
merged.update(block)
89+
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):
103+
found = True
104+
merged.update(block)
98105
return merged, found
99106
100107

0 commit comments

Comments
 (0)