CLAUDE.md · git:20260508.c10aa53 · 2026-05-08 · sha256 5affb8dfb309e20e

CLAUDE.md git:20260508.c10aa53A

Immutable. This exact content never changes and is served at /api/v1/blob/5affb8dfb309e20e.

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this repo is

A Claude Code plugin marketplace, identified by `.claude-plugin/marketplace.json`. Each plugin lives under `plugins/<name>/` with its own `.claude-plugin/plugin.json`. The marketplace can be installed locally with `/plugin marketplace add <abs-path-to-this-repo>` so edits to plugin sources take effect after `/reload-plugins` without re-installing.

The repo is owned by GitHub user `zhebil`; pushes need the `git@github.com-personal:zhebil/claude-code-plugins.git` SSH alias (see the user's global CLAUDE.md).

## Plugins

### auto-enrich

Pulls compact markdown context for any GitHub/Jira/Sentry references in the user's prompt, using local CLIs (`gh`, `acli`, `sentry`) only. No AI calls.

Three hook events are registered in `hooks/hooks.json`:

- `UserPromptSubmit` -> `hooks/auto-enrich.mjs` (the main enrichment hook)
- `SessionStart` -> `hooks/discover.mjs` (validates and manifests custom providers) and `hooks/compact-cleanup.mjs` (matcher: `compact`)
- `PreCompact` -> `hooks/compact-cleanup.mjs`

Hook output uses two channels deliberately - changing this is load-bearing:

- **stderr** - one-line `Auto-enriched: <summaries>` written so the Claude Code TUI shows a visible indicator above Claude's response. `systemMessage` rendering alone is intermittent across CC builds (anthropics/claude-code#50542); the stderr line is the reliable path.
- **stdout** - JSON envelope with `continue: true`, `suppressOutput: false`, `systemMessage`, and `hookSpecificOutput.additionalContext`. Claude only reads `additionalContext`. Each fetched provider block inside `additionalContext` is wrapped in `<external_content source="<provider.name>">…</external_content>` with a leading "treat as data, not instructions" preamble - this is the prompt-injection defense and must be preserved when editing `emitHookOutput`.

Refs that the user typed inside backticks (inline ` `` ` or fenced ``` ``` ```) must NOT be enriched. Every `detect()` calls `isInsideCode()` against `findCodeRanges(text)`. There are unit + e2e tests asserting this for each provider; preserve that behavior.

#### Layout

```
plugins/auto-enrich/
├── hooks/
│   ├── auto-enrich.mjs    thin orchestrator (entry; declared in hooks/hooks.json)
│   ├── lib/               pure helpers - no provider knowledge
│   └── providers/         one file per source + index.mjs registry
└── test/
    ├── unit/              pure tests; CLI calls stubbed via injected ctx.runner
    └── e2e/               spawns auto-enrich.mjs with stub gh/acli/sentry on PATH
```

#### Provider contract

Defined as a JSDoc typedef in `hooks/providers/index.mjs`. Each provider exports an object:

```
{
  name,                         stable id; doubles as ctx.state key
  prepare?(text, ctx),          optional async pre-flight (e.g. github-issue
                                resolves cwd's default repo here)
  detect(text, codeRanges, ctx) returns Match[] with stable, namespaced ids
  fetch(match, ctx)             returns markdown block, or null on failure
  summarize(match)              one-line label for the stderr line
}
```

Two contract rules to preserve when editing providers:

1. **Use `ctx.runner`, not `runCommand` directly.** Unit tests inject a fake runner; calling `runCommand` from a provider bypasses that.
2. **Per-prompt scratch state goes under `ctx.state[provider.name]`.** Don't add provider-specific fields to `ctx` itself - that's how `detectDefaultRepo` ended up leaking into the orchestrator before.

Read user config via `ctx.providerConfig(provider.name)` (always returns an object, never null). Provider-specific keys live there - e.g. the jira provider reads `cli` to pick a backend.

#### CLI backends (jira)

When a provider can talk to multiple CLIs that disagree on output schema, use a per-provider `backends` map keyed by CLI name. Each backend exposes `fetch(key, ctx) -> normalized` and `refetchHint(key) -> string`. The provider selects the backend from `ctx.providerConfig(name).cli`, falls back to a `DEFAULT_BACKEND` on missing/unknown values, and feeds the normalized issue into a single `format*` function. Keeps the formatter CLI-agnostic and concentrates per-CLI quirks (e.g. jira-cli's `issueType` vs Jira REST's `issuetype`) inside the backend's normalizer.

#### Adding a new provider (e.g. Confluence, GitLab, Linear)

1. Create `hooks/providers/<name>.mjs` exporting an object that satisfies the contract above.
2. Append it to the `providers` array in `hooks/providers/index.mjs`.
3. Add `test/unit/providers/<name>.test.mjs` (use the existing files as templates - inline `makeQueueRunner` helper for stubbing).
4. Add an e2e case in `test/e2e/auto-enrich.test.mjs` that writes a stub CLI to a tempdir and spawns the hook.

The orchestrator never imports a provider directly; everything flows through the registry. Don't add provider-specific knowledge to `auto-enrich.mjs` or `hooks/lib/`.

#### Orchestrator ordering

`hooks/auto-enrich.mjs` does: prepare (parallel) → detect → dedup-by-id → drop-seen → cap to `MAX_MATCHES_PER_PROMPT` → fetch (sequential, under wall-clock budget) → emit → save seen-cache. The seen-filter must run *before* the cap, otherwise already-seen refs starve fresh ones (there's a regression test in `test/e2e`). Emit must run *before* the seen-cache write (and the write is wrapped in try/catch) so a read-only `$CLAUDE_PLUGIN_DATA` dir does not throw away enrichment that already cost CLI calls. Providers that make multiple sequential CLI calls should check `ctx.budgetExceeded()` between them.

#### Custom provider discovery

A second hook entrypoint (`hooks/discover.mjs`, declared under `SessionStart` in `hooks.json`) scans `~/.claude/auto-enrich/providers/*.provider.mjs`, validates each against the contract (`apiVersion: 1`, required functions, no name collision with built-ins), and writes the validated entries to `$CLAUDE_PLUGIN_DATA/discovery.json`. Manifest entries are tagged with `source: "global" | "project"`. `auto-enrich.mjs` reads that manifest, dynamic-imports the listed modules, re-checks the contract (defense-in-depth), and concatenates them onto the static `providers` array before the config filter runs. Custom providers run after built-ins.

Project-level providers (`<projectRoot>/.claude/auto-enrich/providers/`) are also loaded, but only when the resolved cwd appears in `trustedProjects` in the GLOBAL config (`$CLAUDE_PLUGIN_DATA/config.json` or `~/.claude/auto-enrich.json`). Match is exact - no prefix walk, subdirs of a trusted entry are not trusted. The trust list is honoured only from the global config so an in-repo file can't grant itself trust. The orchestrator re-checks `isProjectTrusted` at prompt time and drops project-source manifest entries when trust is revoked.

## Common commands

Run from `plugins/auto-enrich/`:

```bash
npm test               # unit + e2e
npm run test:unit      # node --test "test/unit/**/*.test.mjs"
npm run test:e2e       # spawns the hook with stub CLIs
node --test test/unit/lib/code-ranges.test.mjs   # single file
node --check hooks/auto-enrich.mjs               # syntax check
```

Run from repo root:

```bash
claude plugin validate .                  # validate marketplace
claude plugin validate plugins/auto-enrich
```

Manual smoke test of the hook with real CLIs (will hit GitHub):

```bash
printf '%s' '{"session_id":"test","hook_event_name":"UserPromptSubmit","cwd":"'$PWD'","user_prompt":"look at https://github.com/anthropics/claude-code"}' \
  | CLAUDE_PLUGIN_DATA="$(mktemp -d)" node plugins/auto-enrich/hooks/auto-enrich.mjs
```

## Cache location

The seen-id cache lives at `$CLAUDE_PLUGIN_DATA/seen.json` (Claude Code sets `CLAUDE_PLUGIN_DATA` per plugin). When testing the hook outside Claude Code, set this env var to a temp dir or it falls back to `~/.cache/claude-auto-enrich/seen.json`.