CLAUDE.md · git:20260902.0f820d6 · 2026-09-02 · sha256 ee56549afbdd848e
CLAUDE.md git:20260902.0f820d6A
Immutable. This exact content is served forever at /api/v1/blob/ee56549afbdd848e.
# claude-code-hermit (monorepo)
This repo is a multi-plugin Claude Code marketplace. Seven plugins ship from `plugins/<slug>/`:
`claude-code-hermit` (core), `claude-code-dev-hermit`, `claude-code-homeassistant-hermit`, `claude-code-fitness-hermit`, `hermit-scribe`, `laravel-forge-hermit`, `feed-hermit`.
Each plugin has its own `CLAUDE.md`, `CHANGELOG.md`, and `tests/` — read those for plugin-specific context.
The top-level `.claude-plugin/marketplace.json` is the only marketplace. The README at the repo root is the canonical hermit pitch.
Always launch Claude Code from this repo's root, not from inside a plugin dir. Auto-memory is keyed by CWD, and plugin dirs contain their own `.claude-plugin/` (launching there can load the plugin under test as the project plugin). Per-plugin `CLAUDE.md` files load on demand when you touch their files.
## Conventions
- **Per-plugin paths**: every plugin lives at `plugins/<slug>/`. Tests, scripts, skills, agents, hooks, state-templates, docs, CHANGELOG, CLAUDE.md all live inside that dir.
- **Tests run from inside the plugin dir**: core and HA are pure `bun test` (auto-discovers `tests/*.test.ts`; HA keeps Python only as a test fixture, nothing shipped runs it); dev/fitness/scribe/forge use `bash tests/run-all.sh` (forge also needs a composer/PHP step). Helpers use CWD-relative paths and break if invoked from repo root.
- **Tag format**: `<slug>--v<X.Y.Z>` (double-dash, e.g. `claude-code-hermit--v1.0.20`).
- **Independent versioning**: each plugin's `plugin.json` bumps on its own cadence. Domain plugins declare core compat via `required_core_version: ">=X.Y.Z"` (semver range, not pin).
- **Dependency fields**: `required_core_version` and `requires` live in `.claude-plugin/hermit-meta.json` (hermit-internal, validator-invisible). `dependencies` is the native Claude Code resolver field in `plugin.json`. `required_core_version` is authoritative — read by `plugins/claude-code-hermit/scripts/doctor-check.ts` from `hermit-meta.json`. `requires` mirrors it for documentation. Update all three when the core version requirement changes. All hermit-internal manifest extensions (`hermit.*`, etc.) belong in hermit-meta.json. When a domain hatch depends on new core runtime behavior, bump `required_core_version` to the first compatible core version.
- **CC-version-gated work bumps the floor, never shims around it.** When a fix or feature depends on Claude Code CLI behavior introduced at a specific version (a new tool, hook payload shape, flag, or harness fix), bump that plugin's `min_claude_code_version` in `.claude-plugin/hermit-meta.json` to the version that introduces it — don't write feature-detection, version branches, or fallback code paths to keep the change working on older CC. The floor is the contract (enforced at `hermit-evolve` Step 0); raise it instead of carrying legacy compatibility code.
- **Marketplace.json bumps**: only the matching plugin's entry. The release skill takes a slug arg: `/release <plugin-slug>`.
- **Dependency direction is one-way**: domain plugins depend on core; core never depends on them. `plugins/claude-code-hermit/` must not import a sibling, hardcode a sibling slug in logic, or branch on one being installed — it discovers siblings generically (sibling-scan + name-contains-`hermit`) and consumes only what they *declare* (`hermit-meta.json` `hermit.*`). Naming a plugin as an example in prose, docs, or a recommendation list is fine; conditioning behavior on it is not. Reverse direction too: siblings can't import core — shared logic ships as a `hermit-run` verb behind a `required_core_version` floor.
- **Ship mechanism, not policy (all plugins)**: contracts stay strict, content stays loose — an installed hermit belongs to its operator, give skills data + goal + voice and let the model compose (don't hardcode content or operator-workflow opinions), and anything operator-editable must survive `hermit-evolve`. Contract list and extension points: `plugins/claude-code-hermit/CLAUDE.md` § Authorship layers.
- **Ship standard: default-on, research preview.** A new feature ships enabled by default, not behind an opt-in toggle defaulted off — treat it as a research preview to iterate on from real usage, not something held back by a conservative default. Default off only when the feature needs an operator-supplied credential/config it can't have yet, costs real spend per invocation, or is destructive/irreversible.
- **Token discipline (all plugins)**: hook stdout, skill-driven `Read`s, and helper-script output are injected into the operator's context — print verdict-sized digests, and never have a skill `Read` an unbounded surface (JSONL event logs, DBs) directly; front it with a script that returns a bounded summary. The atom of cost is the API call, not the prompt: every tool call re-reads the full accumulated context from cache, so cache traffic is ≈85-90% of an always-on hermit's spend, measured live. Full pattern and examples: `plugins/claude-code-hermit/CLAUDE.md` § Development constraints. (This file is itself the costliest token surface in the repo — loaded every session and re-seeded into every subagent — so keep additions here terse.)
## Commits
- Root-scope edits (CI, root README, `.claude/`, `.claude-plugin/marketplace.json`) skip the CHANGELOG step entirely — they don't ship to operators. `/commit` handles that automatically.
- **Docs-only changes never get a CHANGELOG bullet.** README, `docs/`, `CLAUDE.md`, and code-comment edits (even inside a plugin scope) are not something an operator experiences as a shipped change. Only real code/behavior changes earn an entry. When a task bundles a code fix with an incidental docs correction, only the code fix gets a bullet; the docs correction goes in the commit message and PR description. `/commit` and `/release` enforce this.
- Releases still go through `/release <slug>`, which promotes a plugin's `[Unreleased]` section to a real version. `/commit` accumulates those entries during day-to-day work.
- **Where these skills live**: `/commit`, `/release`, `/release-status`, `/fleet-release`, `/bump-core-req`, `/test-run`, `/tackle-issue`, `/pre-release-review`, `/docs-drift`, `/pipeline-digest` are repo-internal skills under `.claude/skills/` — they're not shipped to operators, only used during monorepo dev. Use `/release-status` for a read-only pipeline snapshot before any release session; use `/fleet-release` when multiple plugins change together on one branch (handles dep ordering and `required_core_version` sync automatically).
- **Changelog style: terse like [Claude Code's CHANGELOG](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md).** Keep the `### Added / Changed / Fixed` headers, but write each bullet as a plain sentence-case line describing the change — the header supplies the category, so no leading Fixed/Added verb and no `- **component:**` prefix. Backticks for commands, paths, and flags; 1–2 lines each. No `### Files affected` tables. Rationale, debugging context, and design tradeoffs go in the commit message and PR description, not the bullet. The verbose form is reserved for `### Upgrade Instructions`, which `hermit-evolve` reads imperative-step-by-step. The `/commit` and `/release` skills enforce this; the convention lives here for any agent that bypasses them.
- **`/release` is operator-initiated — don't auto-suggest it.** Don't propose `/release <slug>` or `/fleet-release` in plans, summaries, or "next steps." Wait for an explicit ship/release/version request. `/release-status` is read-only and fine to suggest when checking pipeline state.
- **Proposals are local-only — never reference them externally.** Proposals (`PROP-NNN`, the local proposal queue) live in this project's hermit state and are internal dev bookkeeping. Never mention a proposal ID or the fact that work came from a proposal in a CHANGELOG entry, commit message, PR title/description, or branch name. Describe the change on its own terms. Branch names use `feat/<N>-<slug>`/`fix/<N>-<slug>` off the GH issue number, not a proposal ID.
- **Never publish Claude Code session URLs.** This is a public repo. Session/transcript links (`claude.ai/...` session URLs, share links, artifact URLs tied to a session) must never appear in a PR title/description, PR comment, issue, CHANGELOG entry, commit message, proposal, or any other externally-visible surface. They can leak internal context and aren't stable references. Describe the work directly instead.
## Branching
- **Default: PR `fix/<N>-<slug>` / `feat/<N>-<slug>` / `chore/<slug>` branches to `main`.** Same flow for contributors and maintainer. `<N>` is the GH issue number (omit if no issue), `<slug>` is a short kebab-case descriptor. Examples: `fix/44-self-update-race`, `feat/57-cortex-tagging`, `chore/upgrade-node-24`. Regular merge to `main`.
- **Why main is safe as staging.** Claude Code's `/plugin update` only fires when `version` in `plugin.json` changes ([docs](https://code.claude.com/docs/en/plugins-reference#version-management)). Commits on `main` between releases are invisible to operators on the standard install path — `/release <slug>` is the actual ship event because it bumps `version` and promotes `[Unreleased]` → `[X.Y.Z]`. Caveats: brand-new installers and `--plugin-dir` testers get whatever is on `main` HEAD, so don't leave `main` knowingly broken for long.
- **Tags ship to operators on next `/plugin update`.** The `version` bump inside `/release` is the gate — pre-release commits on `main` don't reach `/plugin update` users until that bump happens.
- **Worktree discipline (`claude --worktree`).** When the session runs inside a git worktree, the repo root is the worktree (`git rev-parse --show-toplevel`), not the main checkout. Never `cd` into, edit, or `git -C` files under the main checkout or any sibling worktree — that's another session's territory. The only shared, intentionally main-rooted path is `.claude-code-hermit/` (gitignored hermit state). The `claude-code-dev-hermit` `worktree-boundary-guard` hook hard-blocks edits that escape the worktree.
## Layout gotchas
- **Sibling-scan pattern**: `${CLAUDE_PLUGIN_ROOT}/../*/.claude-plugin/plugin.json` resolves to `plugins/*/...` and finds all fleet plugins as guaranteed siblings.
- **Old standalone repos are redirect-only zombies**: `gtapps/claude-code-dev-hermit` and `gtapps/claude-code-homeassistant-hermit` exist but their `marketplace.json` redirects to this monorepo via `git-subdir`. **Do not push code there.** All work happens here.
## graphify
Two graph levels. Pick by scope **before** answering a codebase question.
- **Inside one plugin** (most questions): `graphify query "<q>" --graph plugins/<name>/graphify-out/graph.json`
- **Spanning plugins**, or about `scripts/`, `tests/cross-plugin/`, `marketplace.json`: `graphify query "<q>"` (root graph)
- **The root graph is dominated by `claude-code-hermit`**, so a single-plugin question asked against it spends its budget on core's test harness and never reaches the answer. Always pass `--graph` for single-plugin questions.
- `graphify path "<A>" "<B>"` for relationships between two nodes, `graphify explain "<concept>"` for one node and its neighbours.
- **Refresh every level with `bun run graph`** (AST-only, no API cost). Run it after a pull, or when the graph is older than the last commit: a stale graph answers confidently about deleted code.
- **Worktrees start with no graph on purpose.** A copied graph would be frozen at branch-creation time, containing none of the branch's own work. `hook-guard` silently no-ops when `graphify-out/` is absent and the session falls back to grep, which is always correct. If graph orientation is worth it for a long-lived worktree, run `graphify update .` inside it once: that builds a graph that includes the branch work. `bun run graph` refuses to run in a worktree.
## Environment quirks
- **Secrets:** env vars → `.env` (gitignored); secret files (`.pem` etc.) → `.claude.local/` (gitignored). Never `.claude/` — it's checked in.
- **Docker paths mirror the host** (`${PWD}:${PWD}` mount) — absolute paths are identical inside the container despite the container user being `claude`.
- **`rm -rf` is a native deny** (`Bash(rm -rf *)` and the flag-order / path-prefixed siblings in `permissions.deny`). Use `rm -r` (no `-f`) for scratch cleanup. The native engine does not fold whitespace/`$IFS`/backslash obfuscation; those are classifier-watched, not a second permission engine.
- **Subtree imports are unsquashed**: `git log --first-parent` for the monorepo-only view; full upstream commits live under each subtree merge.
- **CI is path-filtered per plugin**: every plugin has a paths-filtered workflow under `.github/workflows/` (`test-hooks` core, `test-ha`, `test-dev`, `test-fitness`, `test-scribe`, `test-forge`, `test-feed`), so a PR touching one plugin runs only that plugin's suite. Root-file changes (`package.json`, `bun.lock`, `tsconfig.json`) trigger all of them.
- **Shell `cd` persists across Bash calls.** Any `cd` in a Bash call leaves CWD pinned for subsequent Bash calls in the session — affects CWD-relative scripts like `heartbeat-precheck.ts .claude-code-hermit` (silently `SKIP|HEARTBEAT.md missing`) and commands like `git add`. Plugin test runners (`bun test` or `bash plugins/<slug>/tests/run-all.sh` run from inside the plugin dir) end inside `plugins/<slug>/`. Use absolute paths or prefix `cd "$(git rev-parse --show-toplevel)" && …` (resolves to the current tree root — the worktree under `claude --worktree`, the main checkout otherwise — never a hardcoded path).
## Verification
- **Confirm Claude Code's own behavior empirically before building on it.** When a feature or bug depends on how Claude Code itself behaves (what a tool actually returns, the exact shape a hook receives on stdin, how the harness namespaces or loads things, what a setting or slash command really does, how a native feature fires), don't conclude from docs, memory, or assumption. Spin up a tmux session running real Claude Code, exercise the actual tool, feature, or behavior, and read what it does. The empirical verdict from that run is what you build on; if it contradicts the docs or your expectation, the live behavior wins and that gap is the finding.
- **Run the probe with `/probe`.** It owns the tmux session, the sentinel-verdict protocol, and the model choice (haiku by default to keep cost down; classifier and skill-recall questions need sonnet, since haiku has no auto mode and silently falls back to `acceptEdits`). Under `--plugin-dir`, skills load at session start, so edits to a skill mid-probe are not picked up (relaunch to test changed skill text), and `${CLAUDE_PLUGIN_ROOT}` is not substituted (derive the plugin root from the skill's Base directory instead).
- **Auto mode suspends wildcarded-interpreter `permissions.allow` rules** (e.g. `Bash(bun */scripts/*.ts*)`), empirically confirmed, so a hermit's own sealed script allow-list buys nothing there: every such call re-enters the classifier. Details: `plugins/claude-code-hermit/docs/security.md` § Auto-mode Classifier.
## Rules
- Always use Context7 for library/API documentation, code generation, and setup/configuration steps — don't wait for an explicit request.
- Don't overengineer.
- **This hermit is the plugin-dev special case.** When reasoning about utility of features in `plugins/claude-code-hermit/` (the shipped hermit), don't use this hermit's session history as evidence — the operator here maintains the plugin source. Target users are downstream operators who interact via Discord/Telegram and don't open feature branches.