7.0 KiB
C3 spike findings: --setting-sources interaction + overlay file placement
Task: C3 in system-prompt-profiles-tasks.md. Feeds B2.
Method note (important correction mid-spike)
Initial attempts asked the model to self-report whether ORCHESTRATION.md/skills were
injected ("ORCH=yes/no SKILL=yes/no"). This was unreliable — a default run that the
transcript later proved did receive the orchestration injection had the model answer
ORCH=no. Model self-report is not a valid signal for this spike.
Switched to a deterministic signal: the session transcript JSONL at
~/.claude/projects/<slug>/<session_id>.jsonl records hook execution as
attachment.type == "hook_additional_context" entries (one per SessionStart hook that
returned additionalContext) and the full injected skill list as
attachment.type == "skill_listing". Both are populated before the model ever sees the
turn, so they're a ground-truth substitute for /context.
Spike (a): --setting-sources vs symlinked local plugins + absolute-path hooks
4 headless haiku runs from an empty scratch dir
(/tmp/claude-1000/.../scratchpad/c3spike), claude -p --model haiku --output-format json "reply OK", varying --setting-sources. For each, inspected the resulting transcript
JSONL for hook-injected additionalContext count and whether plugin-specific skills
(os-adr, os-vault) appear in the injected skill listing (built-in skills like
autoresearch/caveman appear regardless — only plugin skill presence is diagnostic).
--setting-sources value |
additionalContext entries (hooks fired) | os-adr/os-vault skills present |
|---|---|---|
| (no flag, default) | 1 (confirmed in first exploratory run) | yes |
"" (none) |
0 | no |
user |
1 | yes |
project,local |
0 | no |
Interpretation:
- Hooks in this setup are wired only in
~/.claude/settings.json(a user-scope settings file), by absolute path intocc-os/. They fire if and only if theusersettings source is included.--setting-sources ""and--setting-sources project,localboth silently drop them — no error, no warning, just noadditionalContext. This is a real footgun for a futurecyoloprofile wrapper: any profile that passes--setting-sourcesexplicitly must includeuseror it silently loses ORCHESTRATION.md injection, os-status checks, os-vault hooks, and every other hook in the plugin family. - Plugin skill registration (the
os-adr:findstyle slash-command/skill listing) tracks the same axis: present withuserincluded, absent without it. This makes sense —enabledPluginslives in the user-scope~/.claude/settings.json, so a--setting-sourcesvalue that excludesuseralso excludes plugin enablement, not just hooks. - Symlinks themselves are a non-issue: once the
usersettings source is included, the symlinked plugin (~/.claude/plugins/os-adr -> cc-os/plugins/os-adr) resolves and loads identically to the default case — no special-casing needed for the symlink or for the hooks' absolute paths intocc-os/. The failure mode is entirely about which settings source is included, not about symlinks or path resolution.
Verdict (a): --setting-sources interacts cleanly with symlinked plugins and
absolute-path hooks in the sense that nothing breaks or errors — but it is an all-or-
nothing gate: excluding user from --setting-sources silently disables every cc-os
plugin (hooks AND skills) with no error surfaced. Any profile wrapper must always include
user in --setting-sources (or simply not pass the flag) to keep the plugin family
alive; only project/local inclusion should be varied per profile.
Spike (b): where should per-profile settings overlay files live?
Read bin/refresh-plugins (Ruby). It touches exactly these paths, all read-only except
for the plugin cache itself:
- Reads
~/.claude/plugins/known_marketplaces.json(marketplace list/config) - Reads
~/.claude/plugins/installed_plugins.json(which plugins are installed per marketplace) - For each installed plugin under a
directory-source marketplace, shells out toclaude plugin uninstall <plugin>@<marketplace>/claude plugin install <plugin>@<marketplace>— this rewrites~/.claude/plugins/cache/<marketplace>/ <plugin>/<version>/(via Claude Code's own plugin manager, not directly) and updatesinstalled_plugins.json's install record for that plugin. - Verifies via
diff -rqbetween the cache dir and the plugin's source dir undersource_path(from marketplace config, e.g.~/dev/cc-os/pluginsor~/dev/cc-plugins).
It never touches ~/.claude/settings.json, never touches any file under a
~/.claude/profiles/ style directory, and never touches arbitrary --settings <file>
overlays — its blast radius is strictly known_marketplaces.json,
installed_plugins.json, and the plugins/cache/<marketplace>/<plugin>/ tree.
Recommendation: put per-profile settings overlay JSONs at ~/.claude/profiles/*.json
(e.g. ~/.claude/profiles/cc-os-design.json, client-dev.json, brainstorm.json),
passed via claude --settings ~/.claude/profiles/<name>.json. This path is structurally
disjoint from everything refresh-plugins reads or writes (plugins/cache/,
plugins/known_marketplaces.json, plugins/installed_plugins.json) — there is no
plausible future edit to refresh-plugins that would need to reach into
~/.claude/profiles/, since that script's whole job is cache-vs-source verification for
marketplace-installed plugins, not settings resolution.
Interaction with enabledPlugins: --settings <file> is merged with (not a full
replacement for) the resolved settings from the sources selected by --setting-sources
(per spike (a), keep user in the source list so the base ~/.claude/settings.json
enabledPlugins/hooks/env still apply). The profile overlay should therefore only need
to express a diff — an enabledPlugins map with the profile's unwanted plugins set to
false (or, if this repo prefers positive lists, the full set with only the
wanted ones true) — rather than duplicating the whole settings file. This diff-only
overlay is exactly what B2 should generate per profile. One caution to carry into B2:
verify at build time whether CC deep- or shallow-merges the enabledPlugins object
across --settings sources (not tested in this spike) — either way, a plugin disabled
via the overlay's enabledPlugins: {"name": false} does not require touching
known_marketplaces.json or installed_plugins.json, since enabledPlugins toggles are
independent of the marketplace/install-record identity those files hold (the files
refresh-plugins owns).
Verdict (b): ~/.claude/profiles/*.json (passed via --settings) is safe — it lives
entirely outside the three paths refresh-plugins reads/writes/rewrites
(known_marketplaces.json, installed_plugins.json, plugins/cache/), and
enabledPlugins toggles in an overlay are orthogonal to the marketplace/cache identity
refresh-plugins verifies, so cache refreshes and profile overlays can never clobber
each other.