pi-match-rules0.8.0
A pi extension that syncs Claude Code rules and settings.json hooks into pi. It loads Markdown rules from global and project-local .claude/rules and .agents/rules directories, runs Claude hook commands on the matching pi events, and injects subdirectory A
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-match-rules
A pi extension that syncs Claude Code configuration into pi:
- Loads Markdown rules from global and project-local
.claude/rulesand.agents/rulesdirectories and injects the relevant rules into pi's system prompt. - Injects subdirectory
AGENTS.md/CLAUDE.mdfiles that pi itself never loads, because pi only reads context files from the working directory and its ancestors. - Runs Claude Code hooks from
~/.claude/settings.json(and project-local.claude/settings.json/.claude/settings.local.json) on the matching pi events, so hooks like Orca telemetry or voice-lint work identically in both agents. Project-local hooks require a trusted project.
Why
This is probably a bloated extension (sorry for that). Let me briefly explain the default behavior of pi:
- pi only reads
~/.pi/agent/AGENTS.mdfor user-scope rules - pi only reads
./AGENTS.mdfor repo-scope rules - pi cannot read subdirectory
AGENTS.md - pi cannot apply path-based conditional rule loading (Claude Code, gh copilot, and opencode do)
- pi cannot apply model-decision-based conditional rule loading (antigravity does)
This extension allows pi to:
Read
~/.claude/rules/**/*.md,~/.agents/rules/**/*.md, and~/.pi/agent/rules/**/*.mdfor user-scope rules, as well as.claude/rules/**/*.mdand.agents/rules/**/*.mdfor repo-scope rules. So you have a single source of truth for pi, Claude Code, and other agents for rules. In addition, humans can categorize rules easily.Read
./**/AGENTS.mdwhen pi reads or writes in a subdirectory of your repo.Apply frontmatter patterns for conditional rule loading. For example, you can write
~/.claude/rules/python/testing.md:--- paths: - "**/test_*.py" --- # Pytest rules - Write tests to cover happy path and edge cases - Avoid writing tautology testsThis rule is injected to pi only when it reads or writes
**/test_*.py. The supported keywords are any ofpattern,patterns,path,paths,glob,globs(for compatibility with Claude Code, gh copilot, and opencode, each of which supports different keywords). Without one of these frontmatter keywords, the rule is always loaded.Load conditional rules lazily. For example, you can write
~/.claude/rules/migrations.md:--- pattern: - "migrations/**" - "schema.sql" description_to_model: - "Database migration safety rules, zero-downtime column drops, and idempotent SQL scripts" --- # Database Migration Rules Never drop a column in the same release it is removed from code. All migrations must be backward-compatible with the previous application version.This rule will never appear to pi unless it touches files matching the provided pattern. Once touched, only the
description_to_modelcontent appears. Pi can decide whether its current task is relevant to this rule; if yes, it further loads the rule's body.pattern(and friends) anddescription_to_modelcan be used independently.
This sounds like a skill, but I would rather keep skill as a set of actions to perform a task, while a rule is simply suggestions for the agent's current action or task.
Install
This repository is a pi package. Install it from this checkout for all pi sessions:
pi install ./
Or from npm once published:
pi install npm:pi-match-rules
Use this checkout directly without installing (good while developing):
pi -e ./index.ts
Rule format
Rules are Markdown files with YAML-style frontmatter. Supported keys:
alwaysApply: truepattern/patternspath/pathsglob/globsdescription_to_model(a list of strings, switches the rule to lazy delivery)
Example of an eagerly injected rule:
---
globs:
- "**/*.py"
- "**/pyproject.toml"
---
# Python Rules
Use strict typing and pytest.
Example of a lazily delivered rule:
---
globs:
- "**/*.py"
description_to_model:
- "Python standards, strict typing and pytest conventions"
---
# Python Rules
Use strict typing and pytest.
Rules carry two independent axes. Patterns are the gate and decide when a rule becomes visible at all. description_to_model is the delivery mode and switches from eager body injection to lazy loading, where the model sees a one-line description and pulls the full body through the load_claude_rules tool when the task needs it.
| Frontmatter | Gate | Delivery |
|---|---|---|
| Patterns only | Path match | Body injected on match |
Patterns plus description_to_model |
Path match | Description on match, body loaded through the tool |
description_to_model only |
None, always visible | Description in the index, body loaded through the tool |
Neither key, without alwaysApply: false |
None | Body injected every turn |
alwaysApply: true |
None, override | Body injected every turn, even with a description |
alwaysApply: false without patterns |
Disabled | Disabled |
| No frontmatter | None | Body injected every turn |
Topic rules (a description without patterns) are listed in a Claude rules index appended to the system prompt whenever they exist, even when the prompt mentions no file paths. Pattern-gated lazy rules stay invisible until a matching path shows up in the prompt, and then only their descriptions are shown, with a directive to load the rule before editing the matching files. Index descriptions are truncated to about 160 characters. Since the model decides from the description alone whether to load a rule, write descriptions that state when the rule applies.
Rules are loaded from five directories, in increasing precedence:
- Global:
~/.claude/rules/**/*.mdby default - Global:
~/.agents/rules/**/*.mdby default - Global:
$PI_CODING_AGENT_DIR/rules/**/*.md(default~/.pi/agent/rules), the pi agent directory, so pi-specific rules live next to your other pi configuration - Local:
<project>/.claude/rules/**/*.md - Local:
<project>/.agents/rules/**/*.md
If two directories hold a rule with the same relative path, the directory later in that list wins and the earlier rule is not loaded. Only exact relative-path collisions override; python/api.md, ts/api.md, and api.md are all different rule names. A file reachable from two locations, for example when ~/.agents/rules is a symlink to ~/.claude/rules, is loaded once.
Symlinked rule files and directories are followed, so a rules directory entry can be a symlink into another tree (for example .claude/rules/shared pointing at Claude's rules directory). The rule's identity stays the symlink path under the rules directory. Broken symlinks are skipped, and directory cycles are cut by tracking the real paths already visited.
Tool
The extension registers load_claude_rules, which the agent can call with file paths discovered during the task, or with rule names taken from the Claude rules index.
pathsselects rules by frontmatter path patterns and returns full bodies. A pattern-gated lazy rule matched this way returns its body too, since the call is an explicit request.rulesselects rules by name (relative path likeshared/security.md, basename likesecurity.md, or stem likesecurity) and bypasses path gating, so topic and deferred rules load on demand.- Always-injected rules are included by default and can be excluded with
includeAlways: false.
Tool output is truncated to 50KB or 2000 lines.
Subdirectory AGENTS.md
pi loads AGENTS.md (or CLAUDE.md) only from the working directory and its ancestors, so context files in subdirectories never reach the session. This extension fills that gap. When a tool touches a path under a subdirectory that has its own context file, the file's content is appended to that tool's result, once per file per session.
- Candidates per directory, in priority order, match pi's own list (
AGENTS.override.md, thenAGENTS.md/AGENTS.MD, thenCLAUDE.md/CLAUDE.MD), and only the first present file in a directory is used. - Applies to
read,write,edit,ls,find, andgrepcalls that carry apathinput under the session cwd. Tools registered by other extensions, even ones that take apathinput, are not inspected.bashcommands are not inspected either, since their working directories cannot be resolved reliably. - Paths outside the session cwd are ignored. The cwd's own context file is skipped because pi already loaded it at startup.
- A chain of nested files is injected together, ordered from the directory closest to the cwd outward.
- Appended output is truncated to 50KB or 2000 lines.
/claude-rules reloadclears the injected set, so edited files can inject again later in the session.
Commands
/claude-rulesshow how many rules are loaded./claude-rules reloadreload rule files from disk./claude-rules <path-or-name> [...]show the rules matching paths, or load named rules directly to preview any rule, lazy ones included./claude-hookslist every synced hook with its event, matcher, source file, and command./claude-hooks reloadreload hooks from the settings files.
Configuration
Environment variables, all optional:
PI_CLAUDE_RULES_DIRoverrides the global Claude rules directory (default~/.claude/rules).PI_AGENTS_RULES_DIRoverrides the global agents rules directory (default~/.agents/rules).PI_CODING_AGENT_DIRoverrides the pi agent directory, whoserules/subdirectory is read as another global rules source (default~/.pi/agent). This is the same variable pi itself uses for its configuration directory.PI_CLAUDE_SETTINGS_FILEoverrides the global settings file for hooks (default~/.claude/settings.json).PI_CLAUDE_HOOKS_ENABLED=0disables hook syncing entirely.PI_SUBDIR_AGENTS_MD=0disables subdirectoryAGENTS.mdinjection.
PI_CLAUDE_RULES_DIR=~/my-rules pi -e ./index.ts
PI_AGENTS_RULES_DIR=~/my-agents-rules pi -e ./index.ts
PI_CODING_AGENT_DIR=~/.custom-pi/agent pi -e ./index.ts
PI_CLAUDE_SETTINGS_FILE=~/my-settings.json pi -e ./index.ts
PI_CLAUDE_HOOKS_ENABLED=0 pi -e ./index.ts
PI_SUBDIR_AGENTS_MD=0 pi -e ./index.ts
Glob support
The matcher supports *, **, ?, brace extensions such as *.{ts,tsx}, and character classes such as *[Tt]able*.tsx.
Hooks
The extension also syncs Claude Code hooks into pi. It reads the hooks key from these settings files, in order:
- Global:
~/.claude/settings.json - Project:
<project>/.claude/settings.json - Local:
<project>/.claude/settings.local.json
Hooks from all files run (identical handlers are deduplicated), matching Claude's merge behavior. Only type: "command" hooks run; http, mcp_tool, prompt, and agent handlers are skipped, as are handlers using the if permission-rule filter. A disableAllHooks: true setting in any loaded file disables them. Handlers marked async: true run in the background, like Claude's async hooks. They never block the event, their output is ignored, and failures are logged to stderr.
Project trust
Hooks are shell commands, so the project-local settings files load only when pi trusts the project (ctx.isProjectTrusted()), mirroring how pi gates its own protected project resources. In an untrusted project only the global settings file is read, and a warning names the skipped files. Grant trust with /trust, then run /claude-hooks reload.
Rule files and subdirectory AGENTS.md are Markdown, not commands. They reach the model as instructions, the same untrusted-input channel as pi's own context files, so they load regardless of project trust.
Hooks run through sh -c (or bash when the handler sets shell: "bash"), so hook syncing needs a Unix-like system.
Event mapping
| Claude event | pi event | Notes |
|---|---|---|
SessionStart |
session_start |
source mapped from pi's reason (new to clear, reload to startup, others verbatim); matcher filters the source |
UserPromptSubmit |
before_agent_start |
sends prompt |
PreToolUse |
tool_call |
can block or patch the tool input |
PostToolUse |
tool_result |
only when the tool succeeded |
PostToolUseFailure |
tool_result |
only when the tool errored, with error text |
Stop |
agent_end |
sends stop_hook_active: false and last_assistant_message; also fires when a run was aborted, since pi's agent_end carries no aborted flag |
TeammateIdle |
agent_settled |
|
PreCompact |
session_before_compact |
trigger mapped like PostCompact; sends custom_instructions; cannot cancel compaction, matching Claude |
PostCompact |
session_compact |
trigger is manual or auto; sends compact_summary |
SessionEnd |
session_shutdown |
reason mapped to clear, resume, or other |
Other Claude events (Notification, SubagentStop, PermissionRequest, FileChanged, and the rest) have no pi equivalent and are skipped; /claude-hooks lists them.
Payload and tool translation
Each hook receives Claude's JSON input on stdin (session_id, transcript_path, cwd, hook_event_name, plus event fields). transcript_path is pi's session file, and session_id is pi's session id. CLAUDE_PROJECT_DIR is set to the session cwd.
pi tool names and inputs are translated to Claude's shapes so existing hooks work unchanged:
writebecomesWritewithfile_path;editbecomesEdit/MultiEditwithold_string/new_string;readbecomesRead;bashbecomesBash. Matchers are tested against both the pi and Claude tool names, so"matcher": "Write|Edit|MultiEdit"matches pi'sedittool.tool_responseapproximates Claude's shapes ({stdout, ...}for bash,{filePath, success}for file tools).
Output handling
Hook stdout and exit codes follow Claude's protocol:
- Exit 2 on
PreToolUseblocks the tool call;permissionDecision: "deny"blocks and"ask"prompts the user before running the tool;updatedInputrewrites the arguments. - Exit 2 or
decision: "block"onPostToolUse/PostToolUseFailureappends the reason or stderr to the tool result so the model can react (this is how voice-lint exit-2 feedback works). additionalContext(and plain stdout forSessionStart/UserPromptSubmit) is injected into the conversation; on tool events it is appended next to the result.systemMessageis shown as a warning.
Limitations where pi has no equivalent: a UserPromptSubmit block cannot erase an already submitted prompt, and a Stop block cannot resume a finished run; both surface a warning instead. Timed-out hooks discard their output, matching Claude.