Home / Projects / Orchestrata Ultimate / Decisions / ADR index / ADR-0092
ADR-0092: The orchestrata plugin is self-contained, pinned by a tag, and installed by one verifying command
| field | value |
|---|---|
| Status | Accepted |
| Date | 2026-10-12 |
| Register | factory (docs/adr, 4-digit ids) |
| Source | docs/adr/0092-the-orchestrata-plugin-is-self-contained-pinned-by-a-tag-and-installed-by-one-verifying-command.md — recorded on branch docs/adr-register-0080-0085 at 3f68a11a5, not yet on master |
| Wiki register entry | ADR-0092 (wiki status: Accepted) |
The record
Status: Accepted · Date: 2026-10-12 · Ruled: 2026-10-12 (Mohamed, with decision 7 added)
Context
Session goal (Mohamed, 2026-10-12, verbatim): "workimg instaaller that installs the project as a working and wired claude plugin". Its exit criteria are:
- One command installs the plugin from a pinned git ref into a clean config dir. It exits 0, or it refuses without leaving a half-install.
- A headless session in that config lists the plugin's hooks, skills, commands and agents, and each hook event is seen to fire its plugin hook.
- Every hook and tool runs from the installed copy. A verify step refuses success when the running plugin is not the installed ref.
- Re-running is a no-op, and it upgrades cleanly from the previous ref.
- The tests pass forced: 0 failed, 0 errors, 0 skipped.
- Merged, then installed on this machine from that ref.
An installer exists today: deploy/install-claude-config.sh, 481 lines. It
copies claude/ into ~/.claude/{hooks,tools,agents,skills,commands} with its
own ~/.claude/.venv. It then adds the git marketplace with no ref, installs
the plugin, and checks the installed bytes. Everything below was measured on
2026-10-12 against origin/master = 762935ab6, using Claude Code 2.1.296.
Measured: the plugin works only beside a checkout
The plugin was installed into a clean CLAUDE_CONFIG_DIR with an isolated
HOME and a PATH with no checkout .venv on it. Then claude --init-only --debug hooks ran. That run needs no credentials, and it writes every hook's
exit status to $CLAUDE_CONFIG_DIR/debug/<id>.txt.
Registered 65 hooks from 4 plugins
SessionStart plugin hooks: 18 fired — 13 exit 0, 3 exit 2, 2 exit 1
- 3 × exit 2. These commands run
"$HOME/.claude/hooks/<x>.py", outside the plugin:can't open file '<HOME>/.claude/hooks/platform_conventions_prompt.py'(likewiseagent_mail_inbox.pyandproject_context_refresh.py). - 2 × exit 1. These commands run bare
python3, which resolves to/usr/bin/python3= Python 3.11.11. It cannot parse the 3.14 code:SyntaxError: multiple exception types must be parenthesized(tools/claude_irc_inbox.py:107,hooks/test_run_context.py:150).
hooks.json has 65 commands. 21 use bare python3, 10 use $HOME/.claude/…,
and 8 use command -v python3.14t || command -v python3. Today they work on
this machine only because direnv puts a checkout's .venv/bin first on PATH.
The interpreter running them is a checkout's, which is deploy-in-place.
claude/scripts/gen_plugin.py rewrites only the literal /home/mohamed/.claude/
spelling to ${CLAUDE_PLUGIN_ROOT}. The 10 $HOME/.claude commands escape that
rewrite.
Plugin hooks and tools import third-party packages, so a bare interpreter can
never run them (AST who-imports --source claude/hooks,claude/tools):
neo4jat 5 sites (hooks/context_graph/*,tools/conversation_recall.py);redisat 3 sites (tools/claude_turn_capture.py,tools/conversation_recall.py);pydanticat 1 site (tools/session/models.py).
The skills and commands point outside the plugin too. 3 of 11 skills and 5 of
14 commands name ~/.claude/tools/…. session_cli.py alone is named 16 times
and test_isolation.py 6 times.
Measured: each agent is delivered twice
~/.claude/agents/ and plugins/orchestrata/agents/ hold the same seven files:
auditor, builder, lint-fixer, refactorer, reviewer, test-writer and wirer. A
session therefore lists both auditor and orchestrata:auditor, and likewise
for all seven. The claude/ tree is installed twice: once as ~/.claude
copies, with 46 hooks, 27 tools, 11 skills and 14 commands, and once as the
plugin.
The repo's own 30 project-settings hooks are not duplicated.
project-hooks/_dispatch.sh stands down when CLAUDE_PROJECT_DIR is the
governed repo. All 30 match plugin entries by event and script.
Measured: how pinning, relocation and observation behave
- A tag pins; a sha does not.
claude plugin marketplace add '<url>#<40-hex sha>'fails withfatal: Remote branch 762935ab6… not found in upstream origin.'<url>#session-264-2026-08-12-2023'(an existing remote tag) succeeds. It records"ref": "session-264-2026-08-12-2023", and the cloneHEADis the tag's commita22455f39. The docs say a ref is a "Branch or tag", and that a marketplace added with a ref "updates to the latest commit of that ref".file://URLs are refused:Invalid marketplace source format. CLAUDE_CONFIG_DIRrelocates everything plugin-related. With it set,marketplace add+install -y --jsonwrote the marketplace clone,known_marketplaces.json,installed_plugins.json,cache/…/<version>,settings.jsonenabledPluginsanddebug/all under that dir. The install returned{"command":"install","outcome":"ok",…}. The current installer runs itsclaude plugincalls withoutCLAUDE_CONFIG_DIR, so its--prefixnever reaches the CLI.- The current installer pins nothing. Its own error text says: "the plugin installs origin/master HEAD from the git remote … an older --ref cannot be delivered".
- The cache is keyed by the version string.
plugin.jsonsays1.5.0, so every commit between version bumps lands incache/orchestrata/orchestrata/1.5.0. The installer works around this with uninstall + install. The docs give the version precedence as plugin.json, then the marketplace entry, then the commit sha. - Observing hooks without credentials.
claude --init-only --debug hooksneeds no auth. It runs the Setup and SessionStart hooks and logs each one asHooks: <Event>:<matcher> [<command>] (plugin orchestrata@orchestrata) finished with status <n>. The other events (UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, Stop, SessionEnd) need a model turn, which needs a credential.ANTHROPIC_API_KEYis present in the checkout.env; only the name was checked. - Variables in plugin markdown. Per the docs,
${CLAUDE_PLUGIN_ROOT},${CLAUDE_PLUGIN_DATA}and${CLAUDE_PROJECT_DIR}are substituted in skill, command and agent markdown bodies.${CLAUDE_PLUGIN_DATA}(<config>/plugins/data/<id>/) persists across plugin updates.
Decision (proposed)
-
The plugin is the single delivery path, and it is self-contained.
- Every hook command runs as
"${CLAUDE_PLUGIN_DATA}/venv/bin/python" "${CLAUDE_PLUGIN_ROOT}/<path>"or asbash "${CLAUDE_PLUGIN_ROOT}/<path>". - No command may name
$HOME/.claude,~/.claudeor/home/<user>/.claude. No command may use a barepython3/pythonorcommand -v python3.14t. - Skill, command and agent bodies name tools as
"${CLAUDE_PLUGIN_DATA}/venv/bin/python" "${CLAUDE_PLUGIN_ROOT}/tools/<x>.py". gen_plugin.pyperforms every rewrite.gen_plugin.py --checkfails on any surviving forbidden pattern, inhooks.jsonor in any plugin markdown body.
- Every hook command runs as
-
The plugin owns its venv in
${CLAUDE_PLUGIN_DATA}/venv.- It is free-threaded
python3.14t, built byuvfrom a lock file shipped in the plugin. - A stamp file records the hash of that lock.
- A plugin bootstrap hook builds or refreshes the venv on
SetupandSessionStart, whenever the stamp ≠ the lock hash. If it cannot build, it fails loudly (non-zero, with the reason). It never falls back to another interpreter.
- It is free-threaded
-
The pin is a tag,
plugin-<12-hex sha>.- The ship step creates the tag on the shipped commit and pushes it.
- The installer accepts only
--ref plugin-<sha12>, with the sha matching the tag's commit. - It adds the marketplace as
<origin url>#plugin-<sha12>. - A wrong source or ref is re-pointed, never reported and left.
versionis removed fromplugin.jsonand the marketplace entry, so the cache keys on the commit andclaude plugin updatemoves on every new tag. This premise is measured first. If the cache does not key on the commit, the existing uninstall + install reconciliation stays; no new ruling is needed for that.
-
One installer command:
deploy/install-plugin.sh --ref <tag> [--config-dir DIR] [--verify].- It exports
CLAUDE_CONFIG_DIRfor everyclaudecall. - It adds or re-points the marketplace, then runs
install -y --jsonandenable. - It runs
claude --init-only --debug hooks, which bootstraps the venv and fires SessionStart. - It refuses success unless all of these hold:
- (a)
gitCommitSha= the tag's commit; - (b) the installed cache bytes =
git archive <tag> plugins/orchestrata; - (c) the venv stamp = the lock hash, and
sys._is_gil_enabled()is False; - (d) every SessionStart plugin hook in the debug log finished with
status 0, and their count = the SessionStart count in the installed
hooks.json.
- (a)
The install is staged: the prior state of the entries the installer owns (decision 7a) is kept until (a)–(d) pass. On failure those entries are restored and the exit is non-zero.
--verifymutates nothing. Re-running at the same tag changes nothing; that is asserted by an unchanged tree hash of the plugin dirs. - It exports
-
Every hook event is observed in a test, with a credential from the environment.
- The live test installs into a clean config dir and runs one
claude -pturn that uses Bash, withANTHROPIC_API_KEYpassed by environment (never written to the target dir). - It asserts, from the debug log, that every event declared in
hooks.jsonfired its plugin hooks: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, Stop and SessionEnd. - If an event cannot be provoked headless (PermissionRequest is the candidate), the test STOPs and reports it. It never skips. Treatment is then a separate ruling.
- The live test installs into a clean config dir and runs one
-
The
~/.claudecopies retire after an inventory.install-claude-config.shstops copyinghooks/tools/agents/skills/commandsinto~/.claudeonly after an inventory of every outside reference to~/.claude/{hooks,tools,…}has been resolved. That covers systemd units, other repos' CLAUDE.md and AGENTS.md, opencode and kimi configs, and cron. Each one is rewritten to the plugin path or reported. The copies it previously installed are then removed by its own manifest. Until then, both stay, and the duplicate agents stay visible. -
Other plugins, skills and hooks are left exactly as found (Mohamed's ruling, 2026-10-12).
- Measured on this machine, 2026-10-12.
- Two other plugins are enabled:
tmux-status@tmux-status-dev1.2.0, with hooks on 9 events including SessionStart and skilltmux-icons; andturn-navigator@mohamed-tmux1.1.3, with SessionStart and SessionEnd hooks and skillhelp. ~/.claude/skills/synced/did not come from this repo.~/.claude/settings.jsonregisters one Stop hook,~/.claude/hooks/unshipped_guard.pyunder~/.claude/.venv, which the plugin does not carry.- No orchestrata component name collides today, because plugin
components are namespaced
orchestrata:.
- Two other plugins are enabled:
- (a) Owned entries only.
- The installer writes only what orchestrata owns: its key in
known_marketplaces.json, its key ininstalled_plugins.json,enabledPlugins["orchestrata@orchestrata"]insettings.json, and its owncache/anddata/dirs. - Every other key in those files is preserved, and so is every other
file in the config dir: other plugins' entries, user hooks, skills,
commands, agents and
skills/synced/. - The installer hashes them before and after, and refuses success if any one changed.
- The installer writes only what orchestrata owns: its key in
- (b) Rollback is scoped the same way. A failed install restores only the owned entries, never a whole-directory snapshot. A rollback therefore cannot revert another plugin's change made in the meantime.
- (c) Verify judges only orchestrata.
- Check 4(d) counts only debug-log lines attributed
(plugin orchestrata@orchestrata). - Other plugins' hook results are printed as information. They never fail or pass the install.
- Check 4(d) counts only debug-log lines attributed
- (d) Overlaps are reported, never "fixed". The installer lists, and
leaves unchanged:
- name collisions between orchestrata components and another plugin's or user-level skill, command or agent;
- user or project
settings.jsonhooks that run the same script as an orchestrata plugin hook, which therefore fires twice.
- (e) Retirement (decision 6) removes only manifest entries.
- It deletes only files listed in the installer's own manifest, and only
settings.jsonhooks the installer itself wrote. skills/synced/and every file not in the manifest are never touched.- The user-level
unshipped_guard.pyStop hook enters decision 6's inventory: it moves into the plugin or stays, and either way is reported.
- It deletes only files listed in the installer's own manifest, and only
- (f) Tested with neighbours present.
- A coexistence test seeds the target config dir with a second real
plugin from its git marketplace, a user-level skill and a user
settings.jsonhook. - It asserts their bytes are unchanged after install, re-install at the same tag, upgrade to a new tag, and a forced-failure rollback.
- A coexistence test seeds the target config dir with a second real
plugin from its git marketplace, a user-level skill and a user
- Measured on this machine, 2026-10-12.
Alternatives rejected
- Keep the dual install and only fix the gaps. Rejected because the plugin
would still depend on
~/.claudefiles. A clean config dir could never pass criterion 2, and every agent stays listed twice. - A venv in
${CLAUDE_PLUGIN_ROOT}. Rejected because the plugin root changes on every update and is a byte-checked copy of the commit. A venv there breaks check (b), and it is rebuilt on every update. - Pin by sha in the marketplace URL. Rejected because it was measured to
fail (
Remote branch … not found). - Per-plugin
shaon the marketplace entry. That works only forgithub/url/git-subdirsources, not for the relative-path entry this repo ships. It would also require a marketplace commit per release that names the previous commit. - Build the venv in the installer instead of a plugin hook. Rejected
because a user's own
claude plugin updatewould then leave a stale venv with nothing to notice it. The hook self-heals, and the installer still verifies it. - Only
--init-onlyfor "wired". Rejected because it observes Setup and SessionStart only. Five other events would stay assumed, not observed.
Consequences
- A clean machine needs
git,uv,python3.14t, theclaudeCLI and repo access, and nothing else. No checkout is needed. - On this machine, the plugin stops depending on whichever checkout's
.venvheads PATH. gen_plugin.py --checkbecomes the gate for self-containment. A$HOME/.claudeor bare-python3spelling can no longer ship.- Every ship adds one
plugin-<sha12>tag. Tags are never moved or deleted. - Until decision 6 completes,
~/.claudecopies and plugin components coexist as they do today. - Installing, upgrading or rolling back orchestrata cannot change another plugin, a user skill or a user hook (decision 7). A config dir shared with other plugins is a supported target, not an edge case.
Provenance
- Driven by Mohamed's session goal (verbatim above, set via
/goalon 2026-10-12) and his standing "do not stop till you ship". - Measurements were run by the orchestrator on 2026-10-12 in
scratchpad/pin-probe/(cleanCLAUDE_CONFIG_DIR, isolatedHOME). Debug logs are incfg-master/debug/andcfg-s264/debug/. - Documentation answers came from a claude-code-guide agent quoting code.claude.com: plugins/cli-reference, marketplace-reference, manifest-reference, loading, skills, hooks, env-vars and claude-directory.
- Platform convention 4 (no deploy-in-place; installer from a pinned ref plus a
check that refuses success) is in
~/.claude/CLAUDE.md. - Ruling, 2026-10-12. Mohamed, verbatim: "agreed, take into account if
other plugins or skills or hooks exist". Status Proposed → Accepted.
Decision 7 was added from that ruling. Its measurements were taken the same
day from
~/.claude/plugins/installed_plugins.json,known_marketplaces.json,settings.jsonand the cached plugins'hooks/hooks.json.
Related ADRs
No other ADR is named in this record.
Rulings cited
None.
Specs cited
None (or no spec page exists for the ones named).