# This repo installs its own plugins through apm, not Claude Code's native plugin install ADR-0015 moved plugin **authoring** to apm; ADR-0017 added the flat content mirror that keeps the authored `.apm/` tree discoverable by hosts that install natively. Both are about producing the marketplace. This ADR is about consuming it: how the plugins get onto the machine this repo is worked on. **Status: executed (2026-08-14).** All six packages are installed into `/root/ai-development` by `apm install`; the six native project-scope installs (`claude plugin uninstall @holocron --scope project`) are gone and `.claude/settings.json`'s `enabledPlugins` block is empty. ## Context Until now the repo consumed its own output the same way any user would: `claude plugin install @holocron`, six plugins enabled per-project in `.claude/settings.json`, the `holocron` marketplace registered in `~/.claude/plugins/known_marketplaces.json` with `autoUpdate: true`. That worked. It also meant the repo's dogfooding stopped one layer short of the tooling it publishes: `kyberforge` ships `apm-workflow` and `apm-install` skills describing an install path the repo itself did not take. apm supports both scopes. `apm install --global` deploys to `~/.claude/`; plain `apm install` deploys to the project. Global was rejected deliberately — the switch should be provable in one repo before it changes how every other project on the machine resolves its skills. ## Decision Root `apm.yml` declares all six packages under `dependencies.apm`, each as a `git:`/`path:` object against the holocron remote: ```yaml dependencies: apm: - git: git@git.dev.rkdr.net:Defame1297/holocron.git path: plugins/core ``` `apm install` deploys them to `.claude/skills//` and `.claude/agents/.md`. Three sub-decisions inside that: - **Object form over the `@holocron` marketplace alias.** The alias is shorter and apm resolves it correctly (verified end-to-end against this remote), but it first requires `apm marketplace add`, which writes to `~/.apm/marketplaces.json` — user scope, outside the repo, and absent on a fresh clone. The object form needs nothing beyond the committed manifest. - **Remote source over local path.** apm accepts `path: /root/ai-development/plugins/` as a local dependency, which would make the working tree live instantly. Rejected: it erases the distinction between editing a skill and shipping one, which is the entire point of having a marketplace. The remote form keeps the repo running the same released content every other consumer gets. - **Unpinned against the default branch.** Parity with the `autoUpdate: true` the native install had. apm warns on every install (`6 dependencies unpinned`); accepted knowingly. Pinning is a per-entry `ref:` away once the repo tags releases per package — today `git tag` lists one tag total, so there is nothing meaningful to pin to. ## Consequences **Skills lose their namespace.** apm deploys plain project skills, so `git:git-commits` is now `git-commits` and `kyberforge:skill-audit` is `skill-audit`. This is not configurable — a project skill has no plugin to prefix. Every `:` reference in the repo's own instructions was stale the moment the switch landed; `AGENTS.md` and `CONTEXT.md` are updated. The namespaced form still resolves for anyone installing holocron natively, so skill bodies written for both audiences should name the bare skill. **apm owns `.claude/settings.json`.** (ADR-0019 supersedes the "exactly `{"hooks": {}}`" claim below — once a package ships a hook, apm merges it into that file and the merged entry is apm's own output. The rule that nothing repo-authored goes in the file is unchanged.) `apm audit --ci` replays the install into a scratch tree and diffs it against the worktree. apm's hook integrator writes that file, so the replay expects exactly what apm would have written — `{"hooks": {}}` — and any repo-owned key in it is permanent drift that fails the `apm-audit-ci` pre-push hook. Verified both directions: with the pre-existing `enabledPlugins` block present, `1 of 10 check(s) failed`; reduced to `{"hooks": {}}`, `All 10 check(s) passed`. Nothing was lost in that reduction — `enabledPlugins` was empty after the native uninstall and the only `hooks` entry was an empty `PreToolUse: []` — but it does mean the file is no longer available for repo-owned settings. Machine-specific settings go in the gitignored `.claude/settings.local.json`, which apm does not deploy; shared enforcement belongs in `.pre-commit-config.yaml`, where this repo already keeps it. **`apm_modules/` breaks naive tree walks.** apm materializes a full copy of every dependency there — including this repo's own plugins, `.bats` files and all. The dependency copies resolve their bats helpers relative to their own root, not this repo's, so `tests/run-tests.sh` went from 167 tests passing to `334 tests, 167 failures` on the first install. Both discovery walks (`tests/run-bats.sh`, `tests/run-tests.sh`) now exclude `apm_modules/`, on the find side and on the `git ls-files` side that derives the expected set. Any future script that walks the repo tree needs the same exclusion. **Install output is gitignored; the lockfile is not.** `.claude/skills/`, `.claude/agents/`, and `apm_modules/` are regenerated by `apm install`. Committing the deployed skills would add a third mirror of content ADR-0017 already governs two copies of. `apm.lock.yaml` is committed — it is what makes the install reproducible, and `apm audit --ci` checks it. **MCP survived the switch; hooks were never at risk.** apm read `plugins/bin/.mcp.json` as a self-defined direct-dependency MCP server and configured `obsidian` into the repo's `.mcp.json` unprompted. The `gitea` and `context7` servers were never plugin-provided — they live in `~/.claude.json` and are untouched. Every plugin's `.apm/hooks/hooks.json` is `{"hooks": {}}`, so apm's "contributed no entries to claude settings; skipped" warning on `kyberforge` and `lint` is accurate and harmless. **A `.apm/` edit now needs a round trip.** The dependency resolves from the remote, so an edit is invisible to the running session until it is pushed and the install is refreshed. Under the native install with `autoUpdate` the shape was the same; it was more noticeable here at first because the refresh is a manual step where marketplace auto-update was not — ADR-0019 automates it at `SessionStart`. **Correction (2026-08-14): the refresh command is `apm update`, not `apm install`.** An earlier revision of this paragraph named `apm install`, which is wrong: `apm install` deploys from the pinned `resolved_commit` in `apm.lock.yaml` and does not re-resolve refs (`apm install --force` documents this explicitly — "does NOT refresh refs; use 'apm update' for that"). Running it after a merge redeploys the same content and reports success. **User scope is untouched, deliberately.** `bin@holocron`, `gitea@holocron`, and a stale `hello-world@holocron` remain natively installed at user scope, and every project other than this one still resolves its skills that way. Converting them is a separate decision with a blast radius beyond this repo. ## Alternatives considered - **`apm install --global`.** Verified working in an isolated `HOME`: user-scope deploys land in `~/.claude/skills/` and `~/.claude/agents/`, and it is the only scope where a plugin's `bin/` executables deploy (moot here — every `bin/` in this repo is empty but for a README). Deferred, not rejected: it changes skill resolution for every project on the machine at once. - **Keeping both install paths.** Rejected: the same skill would be present twice under two names, and `.claude/settings.json` cannot hold `enabledPlugins` without failing `apm audit --ci`.