cc-os/docs/plans/c3-spike-findings.md

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 into cc-os/. They fire if and only if the user settings source is included. --setting-sources "" and --setting-sources project,local both silently drop them — no error, no warning, just no additionalContext. This is a real footgun for a future cyolo profile wrapper: any profile that passes --setting-sources explicitly must include user or 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:find style slash-command/skill listing) tracks the same axis: present with user included, absent without it. This makes sense — enabledPlugins lives in the user-scope ~/.claude/settings.json, so a --setting-sources value that excludes user also excludes plugin enablement, not just hooks.
  • Symlinks themselves are a non-issue: once the user settings 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 into cc-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 to claude 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 updates installed_plugins.json's install record for that plugin.
  • Verifies via diff -rq between the cache dir and the plugin's source dir under source_path (from marketplace config, e.g. ~/dev/cc-os/plugins or ~/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.