# 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** — not an application. Nothing here is imported or built; the artifacts are shell hooks, skills and their `bin/` helpers, plus the JSON manifests that make an installer find them.
Every change is therefore judged on two things: does the manifest still resolve, and does the hook still hold its contract.

## Commands

```bash
# Build the same NUL-delimited shell list as CI, including extensionless bin entries.
export RUNNER_TEMP="$(mktemp -d)"
python3 - <<'PYTHON' >"$RUNNER_TEMP/shell-files.z"
import os, pathlib, sys
for f in sorted(pathlib.Path(".").rglob("*")):
    if not f.is_file() or ".git" in f.parts:
        continue
    first_line = f.open("rb").readline()
    is_bash_bin = len(f.parts) == 4 and f.parts[0] == "plugins" and f.parts[2] == "bin" and f.suffix == "" and b"bash" in first_line
    if f.suffix == ".sh" or is_bash_bin:
        sys.stdout.buffer.write(os.fsencode(str(f)) + b"\0")
PYTHON
test -s "$RUNNER_TEMP/shell-files.z"
xargs -0 shellcheck <"$RUNNER_TEMP/shell-files.z"
xargs -0 -n1 /bin/bash -n <"$RUNNER_TEMP/shell-files.z"
(cd plugins/guard-hooks/hooks && for t in *.test.py; do python3 "$t" || exit 1; done)
for t in plugins/*/bin/*.test.py; do python3 "$t" || exit 1; done
python3 plugins/guard-hooks/hooks/<name>.test.py     # a single suite
/bin/bash -n plugins/guard-hooks/hooks/<name>.sh     # macOS system bash 3.2, not your shell's bash
claude plugin validate plugins/<mod>                 # a function-hook mod: manifest, module, state contract
claude plugin test plugins/<mod>                     # runs the mod's *.test.ts against the engine
```

There is no build, no package manager, and no test framework.
Suites are plain `python3` scripts requiring `bash` and `jq`, and `node` for `session-to-md.test.py`; `shellcheck` is optional (`shellcheck-on-edit.test.py` skips its findings checks and asserts the silent no-op instead).
The `claude plugin validate` and `claude plugin test` commands are optional checks for a function-hook mod; they are not part of the default CI gate.
`.github/workflows/ci.yml` runs the shell and Python commands above, shellchecks and `bash -n`-parses every shipped shell script including the extensionless Bash entries under `plugins/*/bin/`, and checks the manifests, skill frontmatter, private references, `bin/` executability, and the demo's quoted guard message.

## Offline Secret Reader

Run `python3 -B .trunk/configs/tests/test_trufflehog_to_sarif.py` for the standard-library reader regressions.
CI runs this exact file separately from the shipped hook suites.
The TruffleHog command uses `--no-verification --no-update` and a local filesystem reader that retains decoded findings without literal source positions.

The reader limits each source read to 1 MiB plus a sentinel byte and retains eight cached source files.
It preserves Unicode columns, reported lines and file-level decoded findings; these limits do not bound total JSONL, result count, offsets, or process memory.
TruffleHog also uses `--fail-on-scan-errors`.
The exact `plugins/guard-hooks/hooks/output-secret-mask.test.py` fixture is excluded only from TruffleHog because both constants were confirmed never issued.
This is a file exclusion, not a value whitelist, so other secrets in that file would also be skipped.
All other linters and all other TruffleHog targets remain enabled.
Run literal and encoded detection controls outside that path, and stop each one-off daemon with `trunk daemon shutdown --monitor=false` in the same checkout.

## Layout contract

```log
.claude-plugin/marketplace.json   # marketplace.json ONLY — no plugin.json here
plugins/<plugin>/
  .claude-plugin/plugin.json      # each plugin carries its own manifest
  hooks/hooks.json                # auto-discovered; commands reference ${CLAUDE_PLUGIN_ROOT}
  hooks/*.sh
  hooks/*.test.py                 # beside its hook, never in a tests/ directory
  hooks/_optout.py                # shared suite helper; leading _ keeps it out of the *.test.py glob
  hooks/register.tsx              # a function-hook mod's module, named under "modules" in hooks.json
  hooks/<mod>.test.ts             # its claude plugin test suite, beside the module
  types/index.d.ts                # a mod's $.state contract, named by "types" in plugin.json
  skills/<skill>/SKILL.md         # frontmatter name must equal the directory name
  bin/<tool>                      # on PATH while the plugin is enabled; executable, with a shebang
  bin/<tool>.test.py              # beside its tool, resolved the same way as a hook suite
```

This mirrors Anthropic's own multi-plugin marketplace (`claude-code-plugins`), which is the layout to check against — **not** `rn-agents-kit`, whose single plugin uses `"source": "./"` and says nothing about nesting.

Two placements are load-bearing:

- A `source` in `marketplace.json` that names a missing directory breaks installation for every user of the marketplace. Add the entry and the directory in the same commit.
- Each suite resolves its hook with `Path(__file__).resolve().with_name(...)`, and imports `_optout` as a plain sibling module. Moving either into its own directory silently breaks that, so they stay next to the scripts.

## Hook contract

Enforced by the suites; break one and the corresponding case fails.

- **Fail open.** Empty, malformed, or key-less input exits `0` with no output. `jq` is parsed with `|| true`, so a machine without `jq` makes every guard stop guarding _silently_ — that is deliberate, and it is why `jq` is documented as a hard prerequisite rather than checked at runtime.
- **`PreToolUse` guards exit `2` to block. `PostToolUse` hooks never block** — they emit `hookSpecificOutput.additionalContext` (a warning) or `hookSpecificOutput.updatedToolOutput` (`output-secret-mask` rewriting a Bash result) and exit `0`.
- **The `CC_GUARD_DISABLE_*` opt-out is checked _after_ `HOOK_INPUT=$(cat ...)`, never before.** A `Write` payload carries the whole file content, so exiting before draining stdin leaves the harness writing to a closed pipe and surfaces a hook error at exactly the moment the user asked for the hook to be off. Pinned by a 200KB-payload case in `hooks/_optout.py`, shared by every suite, that runs the hook behind a real pipe and reads the writer's `PIPESTATUS` — Python's `subprocess` swallows `BrokenPipeError`, so an `input=`-style test cannot see this bug.
- Scripts must parse under macOS's system `/bin/bash` 3.2, not just a newer bash on `PATH`.

## Verifying a change

A passing suite is not on its own evidence that it covers anything.
Before claiming coverage, remove the logic and confirm the suite fails: `exit 2` → `exit 0`, `-ot` → `-nt`, drop the reporting line, or move the opt-out above the read.

Mutate in a way that still parses. `bash` exits `2` on a syntax error, which is indistinguishable from a block, so an unparseable mutant yields a vacuous pass — run `/bin/bash -n` on the mutant first, and treat a mutation that matched nothing (a "skipped" file) as a failed check rather than a pass.

## Releasing

The procedure is `CONTRIBUTING.md` §Releasing, and it is not obvious from the history: plugin versions ride their own feature commit, the release commit is CHANGELOG-only, the PR merges with a merge commit, and the tag goes on that merge commit.
Before cutting anything, compare `git tag` against `gh release list` and finish whatever is behind — the tag and the GitHub release are separate trailing steps and both have been dropped before.

## Working in this repo, the guards are usually live

The same hooks are typically installed in the developing session's own `~/.claude/settings.json`, so they inspect the tool calls used to edit them.
Writing documentation or fixtures that _mention_ a blocked shape — a download-and-execute pipeline, `rm -rf` on the home directory, a secrets filename — gets blocked when the text passes through `Bash`.
Submit synthetic test/documentation content through a file editor when appropriate, but never disguise a denied access or switch tools to defeat it. User-designated development paths and their value-access tradeoff are documented in `docs/environment-access.md`; tests must use synthetic inputs, and agents must not register live paths themselves.

## Provenance

`CREDITS.md` is the single place origin is recorded; a hook or skill never restates its own.
Before committing anything derived from upstream work, read the upstream licence directly (`gh api repos/OWNER/REPO/license -q .license.spdx_id`) rather than inferring it, and add an `origin:` key to that component's frontmatter.
A component may be mostly upstream — `context-budget` is 87% verbatim — but then it ships as a declared redistribution, not as original work: CREDITS.md states the measured overlap and names the copyright holder, and the licence has to permit it. What does not belong here is upstream work presented as this repository's own.
