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>
8.3 KiB
8.3 KiB
Packaging Grimoire for an agent client
This is the maintainer contract for shipping Grimoire into an agent client: what every client package shares, what each one adds, and where a new client's files go. One principle governs it: the server orients, the skills hold the depth, and a client adapter only times delivery and says what that one client needs said.
Claude Code is the only client built today. The layout below is kept so adding another one means adding files, not moving or rewriting any.
Shared by every client package
| Piece | Where | What it is |
|---|---|---|
| The skills | plugin/skills/*/SKILL.md |
Agent Skills (the open SKILL.md format). They state every Grimoire reflex in full and name no client. tests/test_guidance_ownership.py fails if a skill names a particular client, or references anything outside its own folder. Every client package ships this folder verbatim. |
| The MCP server | <base URL>/mcp |
HTTP, Authorization: Bearer <fmcp_ key>. Its _INSTRUCTIONS is a client-neutral orientation — the workflow across tools (≤1,600 chars); each tool's description carries its contract; in-band responses (placement, report_back, systems_hint, the duplicate gate, the guessed-id refusal) fire in every client. |
| The adapter API | <base URL>/api/plugin/* |
Endpoints any client's hooks can call with the same key. GET, so read scope is enough: context (live session state), retrieve (rules, preferences and notes for a message), prior-art (records and shape-ledger hints for code being written), tool-rules (rules for a command about to run), moment-tools (which tools reach a mounted moment), shape-check (definitions written this turn that nobody has judged), claim-session / release-session (task claims), write-scope (which project a pending write lands in, and whether this session may make it), processes (stored Processes to expose as skills). POST, so write scope: moment (rules mounted on the moments a tool call reaches), rule-opened (a rule was read), reply-rules (the finished reply, held for an unopened rule or — when the turn closed a task — for missing completion sections). |
| The API key | Grimoire → Settings → MCP Access | One fmcp_ key per install. Write scope covers the MCP tools and every hook; a read key runs only the GET hooks. |
Added by each client
| Piece | What it holds | Rule of thumb |
|---|---|---|
| A manifest | Name, version, how to reach the MCP server with the key, where the skills and hooks are | Only that client's format. |
| Hooks | Scripts in that client's hook format that call the adapter API at the right moment and print what comes back | Timing and transport only. A hook may print status lines (unconfigured, unreachable, running version) and pointers; guidance text comes from the server or the adapter's own static text. |
| Commands | That client's command wrappers, e.g. a process sync | Optional. |
| Adapter static text | What only that client needs said: its local memory files, its compaction command, its commands | Never a copy of a skill or of _INSTRUCTIONS. The loss guard and ownership registry in tests/test_guidance_ownership.py cover it. |
The worked example: Claude Code
| File | Role |
|---|---|
.claude-plugin/plugin.json |
Manifest: mcpServers.grimoire (HTTP, Authorization: Bearer ${user_config.api_token}), userConfig for the base URL and key, version (minted, never hand-edited; see README). |
../.claude-plugin/marketplace.json (repo root) |
The marketplace entry pointing at ./plugin. |
hooks/hooks.json |
Wires the scripts to Claude Code events. |
hooks/grimoire_session_context.sh |
SessionStart: adapter static text + running version + GET /api/plugin/context; a reload banner after compaction. |
hooks/grimoire_autoinject.sh |
UserPromptSubmit: GET /api/plugin/retrieve for the message. |
hooks/grimoire_prior_art.sh |
PreToolUse on editor writes: GET /api/plugin/prior-art. |
hooks/grimoire_after_write.sh |
PostToolUse on shell commands: the same check for code written through the shell. |
hooks/grimoire_tool_rules.sh |
PreToolUse on shell commands: GET /api/plugin/tool-rules. |
hooks/grimoire_project_fence.sh |
PreToolUse on editor writes, shell commands and Grimoire's tools: GET /api/plugin/write-scope for an act aimed at another project, answered ask with the reason; PostToolUse on enter_project remembers the project a session without one entered. |
hooks/grimoire_moment.sh |
PreToolUse on every tool: the rules mounted on the moments the call reaches, POST /api/plugin/moment; skips tools GET /api/plugin/moment-tools says reach nothing mounted, and Grimoire's own (their responses carry moment_rules). |
hooks/grimoire_reply_check.sh |
Stop: sends the finished reply to POST /api/plugin/reply-rules — the rules mounted on the reply moments, and the reply against every rule's trigger, and — when the turn closed a task — the completion-section check; holds once per rule, with the reason the server returns, and never holds the rewrite. |
hooks/grimoire_shape_check.sh |
Stop: sends the definitions the turn wrote (the write hooks' <sid>.written.ids ledger) to GET /api/plugin/shape-check; blocks once, with the reason the server returns, so the agent judges what it built. |
hooks/grimoire_record_opened.sh |
PostToolUse on get_rule: notes the rule as read in the session ledger and POST /api/plugin/rule-opened. |
hooks/grimoire_record_outcome.sh |
PostToolUse on rule_outcome: notes the rule as acted on, locally, for the session-scoped readout. |
hooks/grimoire_claim_session.sh |
PostToolUse on update_task / add_task_log: GET /api/plugin/claim-session, so the task comes back after a compaction. |
hooks/grimoire_precompact_preserve.sh |
PreCompact: tells the summarizer what to keep literally. Local, and never blocks. |
hooks/grimoire_session_end.sh |
SessionEnd: GET /api/plugin/release-session releases the session's claims. |
hooks/grimoire_sync_processes.sh + commands/sync.md |
GET /api/plugin/processes → ~/.claude/skills/grimoire-proc-* stubs; /grimoire:sync on demand. |
hooks/grimoire_defs.sh |
Shared shell helpers: config, dedup ledgers, outage line. |
hooks/grimoire_static_context.md |
The adapter static text. |
Hook config arrives as CLAUDE_PLUGIN_OPTION_API_ENDPOINT / CLAUDE_PLUGIN_OPTION_API_TOKEN (uppercased by Claude Code), with GRIMOIRE_URL / GRIMOIRE_TOKEN as an override.
How other clients would map
Researched, not tested. Re-check each client's current docs before building.
- Agent Plugins 1.0 (Codex, Cursor, GitHub Copilot / VS Code, Kiro, ChatGPT): a root
plugin.json, askills/folder, and anmcp.json. That is the sameplugin/skills/plus two small files. Hooks, commands and rules are not part of v1; each client adds its own under a reverse-domain directory (e.g.com.<client>/) that other clients ignore. - Gemini CLI:
gemini-extension.jsonconfiguring the MCP server, a context file, and bundled skills. - Client-native formats also exist (
.codex-plugin/,.cursor-plugin/) where a client wants more than the shared standard carries. - Claude Code does not read Agent Plugins: only
.claude-plugin/plugin.json.
Open questions for whoever builds the second package
- Passing the key to the MCP server. Claude Code substitutes
${user_config.api_token}into the header. Agent Plugins'mcp.jsonand Gemini's extension config have their own variable and secret handling. Decide per client, and keep the key out of files that get committed. - Coexistence in one folder. Whether a root
plugin.json(Agent Plugins) beside.claude-plugin/plugin.jsonchanges how Claude Code loads the plugin is untested. Test it before shipping both fromplugin/, or give the second client its own package directory that reusesplugin/skills/. - Hook parity. Other clients' hook events differ and are partly implemented. Map each Grimoire hook to the nearest event, and where none exists, leave that timing to the in-band server responses rather than writing a copy of the guidance.
- Process skills. The Claude Code sync writes
~/.claude/skills. Another client needs its own skills location, or the MCP skills extension (SEP-2640) once a client supports it.