CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Failing after 1m27s
CI & Build / integration (push) Successful in 2m1s
CI & Build / Build & push image (push) Skipped
plugin/hooks/grimoire_project_fence.sh runs as PreToolUse on editor writes, Bash and Grimoire's tools. A session belongs to the project its starting directory resolves to (CLAUDE_PROJECT_DIR: a .grimoire marker, else the git remote's binding). A session that starts outside any project belongs to the first one it enters, which PostToolUse on enter_project records. These acts stop on permissionDecision "ask", with the server's reason naming the other project: - an edit in another project's repo or a worktree of it; - a git commit, push, merge, rebase, pull, cherry-pick, revert, tag, am, reset or worktree add there, found through git -C, a cd before it, or the shell's current directory; - a Grimoire write that lands in another project. A live probe showed "ask" holds in bypassPermissions. Filing an issue and every read go ahead. An edit or git command in the session's own repo, and any tool named like a read, never calls the server. Grimoire writes send only their short arguments, so the URL stays small. When the instance doesn't answer, the act goes ahead and the hook says once per outage that it couldn't check. check_plugin.py now holds one outage line per hook and has a smoke event for the fence. GET /api/plugin/write-scope takes fallback_project_id for the entered project. tests/test_project_fence.py drives the hook against real git repos and a stand-in server. tests/test_write_scope.py holds the hook's FENCE_READ_PREFIXES equal to services/write_scope.READ_PREFIXES. The plugin README and PACKAGING describe the fence, and the plugin is minted to 2026.11.05.0245. Milestone 583, step 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
9.7 KiB
Markdown
159 lines
9.7 KiB
Markdown
# Grimoire plugin for Claude Code
|
|
|
|
Turns a self-hosted [Grimoire](https://git.fabledsword.com/bvandeusen/FabledGrimoire)
|
|
instance into a first-class Claude Code extension:
|
|
|
|
- **MCP tools** over your notes, tasks, projects, milestones, systems, and
|
|
rulebook (the `grimoire` server).
|
|
- **Session-start push channel** — a `SessionStart` hook injects the
|
|
active project's live state (from the server) and this adapter's short
|
|
Claude Code guidance, so Grimoire surfaces *without being asked*. Rules are
|
|
never preloaded; they arrive by retrieval when your work matches one.
|
|
- **Prior-art recall on writes** — a `PreToolUse` hook on Write/Edit checks the
|
|
file about to be written against your recorded snippets (what's kept at that
|
|
path, and what resembles the code) and offers them before the helper is
|
|
rewritten. Titles only, never blocks the edit.
|
|
- **The shared Grimoire skills** — client-neutral Agent Skills, the same files
|
|
any client's package would ship: using-grimoire, writing-plans, reporting-back
|
|
(reply to the operator in a shape that says where the work stands),
|
|
systematic-debugging, verification, brainstorming, reusing-code (record and
|
|
recall reusable code as snippets), shape-accounting and family-canon.
|
|
Replaces superpowers.
|
|
- **Your Grimoire Processes as skills** — saved Processes are synced into local
|
|
`~/.claude/skills/grimoire-proc-*` stubs that auto-surface by relevance; the
|
|
stub fetches the live procedure via `get_process`. Refreshed each session and
|
|
on demand with `/grimoire:sync`.
|
|
|
|
It is designed so you can uninstall `superpowers` and depend on Grimoire instead
|
|
of auto-memory — leave auto-memory at its default; Grimoire replaces its job by
|
|
holding the one copy, not by switching it off.
|
|
|
|
**How the pieces divide the work**: the Grimoire server orients
|
|
every MCP client and serves live state; the skills in `skills/` state every
|
|
reflex in full and name no client; this plugin is the Claude Code adapter —
|
|
hooks that deliver at the right moment, `/grimoire:sync`, and the few things only
|
|
Claude Code needs said (`hooks/grimoire_static_context.md`). Packaging Grimoire for
|
|
another client: see [PACKAGING.md](PACKAGING.md).
|
|
|
|
## Install
|
|
|
|
The plugin ships inside the Grimoire app repo, so the marketplace *is* that repo —
|
|
you always get the plugin version that matches your Grimoire instance.
|
|
|
|
```
|
|
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledGrimoire.git
|
|
/plugin install grimoire@grimoire-plugin
|
|
```
|
|
|
|
On install you'll be asked for:
|
|
|
|
| Setting | What |
|
|
|---|---|
|
|
| **Grimoire base URL** | e.g. `https://grimoire.example.com` (no trailing slash) |
|
|
| **Grimoire API key** | a write-scoped `fmcp_` key from **Settings → MCP Access** (stored in your OS keychain) |
|
|
| **Active project id** | optional — numeric project id to scope the session-start context |
|
|
|
|
## What gets wired
|
|
|
|
- `plugin.json` `mcpServers` → the `grimoire` MCP server at `<base URL>/mcp` (Bearer auth).
|
|
- `hooks/hooks.json` → SessionStart hook (`hooks/grimoire_session_context.sh`),
|
|
**fail-open**: if Grimoire is unreachable it injects nothing and never blocks
|
|
the session.
|
|
- `hooks/hooks.json` → PreToolUse hook on `Write|Edit`
|
|
(`hooks/grimoire_prior_art.sh`) → `GET /api/plugin/prior-art`. Returns
|
|
`additionalContext` with **no** permission decision, so it can inform the write
|
|
but never stop it; silent when nothing is recorded, which is most of the time.
|
|
Two framings: a REUSE menu (similar/nearby records), and a SYNC nudge when a
|
|
snippet records the exact file being edited — "updating the record is part of
|
|
the edit" — each with its own once-per-session dedup. A third, ledger-fed
|
|
line names a duplicate family (no canon) or a canon recorded elsewhere for
|
|
the names being written (its own dedup channel, `exclude_derive`).
|
|
Fail-open but not fail-silent: a configured instance that does not answer
|
|
in time is said, once per outage ("Grimoire did not answer … this write went
|
|
UNCHECKED"), so a session can tell "checked, nothing there" from "never
|
|
checked"; an answer clears the marker. The local by-name arm needs no
|
|
server and always runs. Toggle in **Settings → General → Knowledge
|
|
auto-inject**.
|
|
- `hooks/hooks.json` → UserPromptSubmit hook (`hooks/grimoire_autoinject.sh`) →
|
|
`GET /api/plugin/retrieve`: the rules, preferences and records that match the
|
|
message, as a short menu of titles and matched passages.
|
|
- `hooks/hooks.json` → PreToolUse hook on `Bash` (`hooks/grimoire_tool_rules.sh`)
|
|
→ `GET /api/plugin/tool-rules`: the rules that match the command about to run.
|
|
- `hooks/hooks.json` → PreToolUse hook on editor writes, `Bash` and Grimoire's
|
|
tools (`hooks/grimoire_project_fence.sh`) → `GET /api/plugin/write-scope`:
|
|
holds a session to its own project. A session belongs to the project its
|
|
starting directory resolves to (a `.grimoire` marker, else the git remote's
|
|
binding), or, started outside any, to the first project it enters. Editing
|
|
another project's files, a git commit, push, merge, rebase, pull, tag or
|
|
reset in its repo (from `git -C`, after `cd`, or in a worktree of it), and a
|
|
Grimoire write that lands in another project each stop on a permission
|
|
prompt naming that project, in every permission mode. Filing an issue there
|
|
(`create_task(kind="issue")`) and every read go ahead. An edit in the
|
|
session's own repo never calls the server. It can't see a non-git shell
|
|
write into another repo (`sed -i`, a redirect), but it fences that repo's
|
|
commit and push. When the instance doesn't answer, the act goes ahead and
|
|
the hook says so once per outage.
|
|
- `hooks/hooks.json` → PreToolUse hook on every tool (`hooks/grimoire_moment.sh`)
|
|
→ `POST /api/plugin/moment`: the rules mounted on the moments the call
|
|
reaches. It skips tools that `GET /api/plugin/moment-tools` says reach
|
|
nothing mounted, and Grimoire's own tools, whose responses carry
|
|
`moment_rules`.
|
|
- `hooks/hooks.json` → PostToolUse hooks on Grimoire's `get_rule`,
|
|
`rule_outcome`, `update_task` and `add_task_log`: they keep the session's
|
|
ledgers of rules opened and acted on (`get_rule` also reports to
|
|
`POST /api/plugin/rule-opened`), and claim the task being worked
|
|
(`GET /api/plugin/claim-session`) so it comes back after a compaction.
|
|
- `hooks/hooks.json` → PostToolUse hook on `Bash`
|
|
(`hooks/grimoire_after_write.sh`): code written through sed/heredocs/scripts
|
|
never reaches the PreToolUse hook, so this one diffs the working tree after
|
|
every Bash call (per-session path+blob snapshot; one `git status` when
|
|
nothing changed) and runs the same arms on the definitions just written,
|
|
through the same endpoint and the same dedup channels. `additionalContext`
|
|
only; never blocks, and shares the pre-write hook's once-per-outage "did not
|
|
answer" line (8 s budget here — it runs after the tool, so it gates
|
|
nothing). The extractor, the prose/data skip list, the local by-name
|
|
duplicate arm and the outage line are shared in `hooks/grimoire_defs.sh`.
|
|
- `hooks/hooks.json` → Stop hook (`hooks/grimoire_reply_check.sh`): sends the
|
|
reply that ends the turn to `POST /api/plugin/reply-rules`, which holds it
|
|
once for an unopened rule mounted on the reply moments or matching the reply.
|
|
When the turn closed a Grimoire task (`update_task`/`create_task` with status
|
|
done), the same request has the server check the reply for the completion
|
|
sections — where the work sits, what needs you, what comes next. If they are
|
|
missing it blocks once with the reason the server returns, and records how
|
|
the rewrite came out; it never blocks twice, and never blocks when the
|
|
instance did not record the check (unconfigured or unreachable). Outcomes
|
|
land in the admin logs under category `plugin`, action `report_check`.
|
|
- `hooks/hooks.json` → a second Stop hook (`hooks/grimoire_shape_check.sh`): the
|
|
write hooks note every definition a write names (and every new file) in a
|
|
session ledger; at the end of the turn this sends them to
|
|
`GET /api/plugin/shape-check`, which answers with the ones nobody has judged.
|
|
If any are, it blocks once with the server's reason, asking the agent that
|
|
wrote them to say what each is — reusable (record a snippet), an instance or
|
|
variant of one, or a one-off — in one `classify_shapes` call. Same discipline
|
|
as the report check: never twice, never without a recorded check. Outcomes
|
|
land under category `plugin`, action `shape_check`.
|
|
- `skills/` → the universal process-skills, surfaced by description match.
|
|
- `hooks/grimoire_sync_processes.sh` (a 2nd SessionStart hook) + the `/grimoire:sync`
|
|
command → generate `~/.claude/skills/grimoire-proc-*` stubs from your Grimoire
|
|
Processes (via `GET /api/plugin/processes`); also **fail-open**, and pruned to
|
|
match what exists in Grimoire.
|
|
- `hooks/hooks.json` → PreCompact hook (`hooks/grimoire_precompact_preserve.sh`):
|
|
tells the summarizer which parts of the conversation to keep literally. Local
|
|
only, and it never blocks, because a blocked compaction is skipped outright.
|
|
- `hooks/hooks.json` → SessionEnd hook (`hooks/grimoire_session_end.sh`) →
|
|
`GET /api/plugin/release-session`: releases the session's task claims.
|
|
|
|
## Notes
|
|
|
|
- **Do not hand-edit `version` in `.claude-plugin/plugin.json`.** It is minted
|
|
from the clock — run `python3 scripts/mint_plugin_version.py` (or `make
|
|
mint-plugin`, where `make` is installed) after changing anything under
|
|
`plugin/`, and commit the result. The installer decides whether to refresh
|
|
the cache it executes from by comparing that string, so content that ships
|
|
without a new version reaches the repo and stops there. CI fails the lane if
|
|
you forget.
|
|
- The plugin uses one key for its MCP tools and its hooks, so give it **write**
|
|
scope: the MCP tools create and update records, and the moment, rule-opened
|
|
and reply hooks POST. A read-scoped key runs the GET hooks only; the others
|
|
fail open and add nothing.
|