# Plugin-first install — CCAM usable straight from a Claude Code plugin install

Date: 2026-08-10
Status: approved (design)

## Goal

On a machine with nothing but Claude Code installed, these two commands must
leave the user with a working CCAM — hooks flowing, server up, dashboard
reachable, `ccam` CLI on PATH, MCP tools connected:

```
/plugin marketplace add <ccam repo>
/plugin install ccam@claude-code-agent-monitor-plugins
```

No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
`npm start`. This mirrors how `claude-mem` installs.

## Why the current state does not do this

- The only plugin with a server connection, `ccam-dashboard`, declares its MCP
  server as `../../mcp/build/index.js` (`plugins/ccam-dashboard/.mcp.json`).
  Plugins installed from a marketplace subdir are cached as **that subdir
  only** — verified against `~/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/`,
  which contains the plugin directory's contents and nothing above it. The
  relative path escapes the cached tree and resolves to nothing.
- Every plugin's hooks and MCP config assume a dashboard is already listening
  on `localhost:4820`. Nothing starts one.
- Hook installation today mutates `~/.claude/settings.json` through
  `scripts/install-hooks.js`, which requires a checkout to point at.

## Approach: declare the whole repo as one plugin

Add a **root-level** plugin named `ccam` with `"source": "."` in
`.claude-plugin/marketplace.json`. Claude Code then caches the *entire repo*
into `~/.claude/plugins/cache/<marketplace>/ccam/<version>/` — verified against
`~/.claude/plugins/cache/caveman/`, whose cached tree contains the full source
repo (`src/`, `tests/`, `benchmarks/`), because that marketplace declares its
plugin with `"source": "./"`.

So `server/`, `client/`, `mcp/`, `scripts/` and `bin/ccam.js` are all present
under `${CLAUDE_PLUGIN_ROOT}` the moment the plugin is installed. No second
clone, no credentials beyond the marketplace add itself, no committed copy of
the server, no dependency on undocumented cache internals beyond the documented
`${CLAUDE_PLUGIN_ROOT}` variable.

The ten existing subdirectory plugins stay exactly as they are.

Repo working tree is 23 MB — an acceptable cache footprint.

## Components

### 1. Root plugin manifest — `.claude-plugin/plugin.json`

New file, alongside the existing `marketplace.json`. Declares name, metadata,
inline `hooks`, and paths to commands. Hooks are inlined in `plugin.json`
(as `caveman` does) rather than in a root `hooks/hooks.json`, so the repo root
gains no new top-level directories.

`.claude-plugin/marketplace.json` gains one entry:

```json
{ "name": "ccam", "source": ".", "description": "...", "tags": [...] }
```

### 2. Hooks — inline in `plugin.json`

All eight hook types currently installed by `scripts/install-hooks.js`
(`install-hooks.js:86,93`):

- with `"matcher": "*"` — `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, `Notification`
- without a matcher — `SessionStart`, `SessionEnd`, `UserPromptSubmit`

Each runs:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/hook-handler.js" <HookType>
```

`scripts/hook-handler.js` is used unchanged: it is dependency-free (`node:http`
only), resolves ports via `server/lib/server-info.js` — present in the cached
tree — falls back to 4820, and already fails silently so a not-yet-booted
server never blocks Claude Code.

`SessionStart` additionally runs the bootstrap (below).

Consequence: `npm run install-hooks` becomes unnecessary for plugin users, and
uninstalling the plugin removes the hooks cleanly. It stays supported for
checkout users.

### 3. Bootstrap — `scripts/plugin-bootstrap.js` (new)

Runs from the `SessionStart` hook. Never blocks a session.

Runtime state lives under `~/.claude/agent-dashboard/runtime/`, reusing the
existing data-dir convention (`server/lib/claude-home.js:37`, where the SQLite
DB already lives) rather than inventing a new directory. `/.ccam/` at the repo
root is an unrelated lane profile and must not be confused with it.

Sequence:

1. **Fast path.** Read `runtime/state.json`. If the recorded plugin version
   matches and a live server is found via `~/.claude/.agent-dashboard.json`
   (the existing PID-checked discovery file), exit in milliseconds.
2. **Node version gate.** `server/db.js` `require`s `better-sqlite3` — which is
   *not* in `dependencies` — and falls back to `node:sqlite`, available only on
   Node >= 22.5. On anything older the server cannot start at all. Refuse with
   one clear line rather than letting the server crash.
3. **Lock.** `runtime/.bootstrap.lock` created with an atomic `mkdir`, holding
   the PID. A lock whose PID is dead, or older than 10 minutes, is stale and
   reclaimed. Prevents concurrent sessions racing an install or double-spawning
   the server.
4. **Install.** `npm install --omit=dev --ignore-scripts` with the dependency
   tree written to `runtime/node_modules`, never into the plugin cache
   directory. Two reasons: `~/.claude/plugins/cache/` shows GC machinery
   (`.in_use` markers, `.last_inuse_sweep`), and every plugin update creates a
   fresh version directory, discarding anything installed into the old one.
   `--ignore-scripts` is required because `scripts/postinstall.js` otherwise
   runs a full `client/` install, pulling the whole Vite dev toolchain.
5. **Legacy hook cleanup.** If `~/.claude/settings.json` still carries CCAM hook
   entries from a previous `npm run install-hooks`, remove them and print one
   line. Without this, every event is POSTed twice and token/cost figures
   double. Detection reuses `isOurEntry()` from `scripts/install-hooks.js`.
6. **CLI on PATH.** Symlink `bin/ccam.js` to `~/.local/bin/ccam`. Skills and
   commands throughout the repo (`ccam stage`, `ship-feature-lane`) shell out
   to `ccam` and fail without it.
7. **Start server.** Spawn `server/index.js` detached, `stdio: "ignore"`,
   `unref()`, with `NODE_PATH` pointed at `runtime/node_modules`. The hook
   returns immediately; the first install (1–3 minutes) proceeds in the
   background and prints progress to a log under `runtime/`.
8. **Record state.** Write `runtime/state.json`.

The client UI bundle is *not* built here. Hooks, the API and MCP do not need
it; it is built lazily by `/ccam-open`.

### 4. Client dist relocation — `server/index.js`

`server/index.js:156` hardcodes `path.join(__dirname, "..", "client", "dist")`.
Since the plugin cache tree must be treated as read-only, add a
`DASHBOARD_CLIENT_DIST` env override, defaulting to the current path so
checkout behavior is unchanged.

The bootstrap always starts the server with `DASHBOARD_CLIENT_DIST` set to
`runtime/client-dist/`, whether or not a bundle is there yet — a missing
directory serves the API fine and only the UI route 404s. `/ccam-open` builds
into that same directory, so no server restart is needed to pick the bundle up.

### 5. MCP — commit `mcp/build/`

Claude Code starts a plugin's MCP servers as the session opens and offers no
"not ready yet, retry" state, so an async bootstrap cannot win that race.
`mcp/build/` is therefore committed (removed from `.gitignore` and `mcp/.gitignore`)
and the root `.mcp.json` points at `${CLAUDE_PLUGIN_ROOT}/mcp/build/index.js`
with `CCAM_DASHBOARD_URL` defaulted to `http://localhost:4820`.

Cost: the build artifact must stay in sync with `mcp/src`. A pre-commit check
fails when `mcp/src` is newer than `mcp/build`.

The existing `plugins/ccam-dashboard/.mcp.json` keeps its broken relative path
fixed to `${CLAUDE_PLUGIN_ROOT}` form as part of this work, so the subdir plugin
is not left in a knowingly broken state.

### 6. Commands — `plugins/ccam/commands/`

The plugin root is the repo root, so `plugin.json` declares an explicit
`"commands": "./plugins/ccam/commands"` path. This keeps the new command files
inside `plugins/`, next to the ten existing plugins, instead of adding a
top-level `commands/` directory to the repo root.

- `/ccam-doctor` — bootstrap state, Node version, server liveness, DB path,
  duplicate-hook detection, CLI symlink, MCP build freshness.
- `/ccam-update` — refresh dependencies and restart the server after a plugin
  update.
- `/ccam-open` — lazily build the client bundle if missing, then open the
  dashboard.

## Data and lifecycle

| Thing | Location | Survives plugin update |
|---|---|---|
| SQLite DB, transcripts | `~/.claude/agent-dashboard/` (unchanged) | yes |
| node_modules, client dist, logs, lock, state | `~/.claude/agent-dashboard/runtime/` | yes (re-verified each boot) |
| Source, hooks, MCP build | plugin cache version dir | no — re-bootstrapped |

Bootstrap must not set `DASHBOARD_DATA_DIR`. Leaving it at the default keeps a
plugin-run server and a developer's `npm run dev` server on the same data
directory, where `ingestGroupKey` in `server/lib/server-info.js` already
deduplicates hook ingest to a single port.

## Failure modes

**High severity**

- *SessionStart blocks the session.* The first install takes minutes. Mitigated
  by full detachment (`detached`, `stdio: "ignore"`, `unref()`) and an
  immediate hook return. Verified by timing the hook on a cold `HOME`.
- *Duplicate hooks double-count cost.* Events carry no id, so ingest cannot
  deduplicate. Mitigated by step 5 of the bootstrap plus a `/ccam-doctor` check.
- *MCP unavailable at session start.* Resolved by committing `mcp/build/`.

**Medium severity**

- *Two sessions spawn two servers* → `EADDRINUSE`. Mitigated by the lock, with
  a stale-lock timeout so a crashed bootstrap does not wedge every later
  session.
- *`ccam` not on PATH* when `~/.local/bin` is absent from the user's PATH.
  `/ccam-doctor` reports it and prints the line to add.
- *Windows.* `npm.cmd` resolution (already handled in `scripts/postinstall.js`
  via `shell: true`), detached spawn semantics, and `mkdir` locking are the
  least-tested paths.

**Low severity**

- Node 20 users are refused rather than broken; the message names the required
  version.
- 23 MB cache footprint.
- Marketplace mixing one root-source plugin with ten subdir plugins is
  unverified in this combination; it is the first thing the plan verifies, and
  the fallback is a separate marketplace file for the root plugin.

## Verification

1. Install the local marketplace and confirm
   `~/.claude/plugins/cache/<mp>/ccam/*/server/` exists — this gates everything
   else.
2. `npm run test:server` for the `DASHBOARD_CLIENT_DIST` override.
3. Unit tests for bootstrap: fast path, stale-lock reclaim, Node version gate,
   legacy-hook detection — all with injected paths, no real `$HOME` writes.
4. Cold-machine smoke: `HOME=$(mktemp -d)` with no checkout on PATH, install the
   plugin, wait for bootstrap, `curl /api/health`, fire one synthetic hook, and
   assert the event lands in the DB.
5. `npm run mcp:typecheck && npm run mcp:build` for the committed artifact.

## Out of scope

- Publishing to npm.
- Docker-based bootstrap.
- Changing the ten existing subdir plugins beyond the `.mcp.json` path fix.
