115 lines
7.0 KiB
Markdown
115 lines
7.0 KiB
Markdown
|
|
# C3 spike findings: `--setting-sources` interaction + overlay file placement
|
||
|
|
|
||
|
|
_Task: C3 in [system-prompt-profiles-tasks.md](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.
|