Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Normalize line endings: store LF in git, check out LF on every platform.
# Required so biome's --check passes on Windows (default core.autocrlf=true).
* text=auto eol=lf

# Explicit binary types
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.zip binary
*.tgz binary
*.gz binary
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
node: ["20", "22"]
steps:
- name: Checkout
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

- Add `PostToolUse` matching for MCP filesystem write, edit, and multi-read payloads.
- Harden dynamic hook coverage for additional-context JSON output, disabled/static modes, failed tool responses, and duplicate suppression.
- Remove redundant apply_patch path scanning and stale tracked-tool constants.
- Use portable Codex hook interpolation and add package smoke coverage for hook entrypoints.
- Cap recursive rule directory scans and run CI on Windows in addition to Ubuntu and macOS.
- Replace the external glob matcher dependency with an internal matcher so clean Codex plugin installs run without `node_modules`.

## 0.1.0 - 2026-05-15

- Port `pi-rules` rule loading, matching, formatting, truncation, and deduplication to a Codex plugin.
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ Codex plugin that injects local project rule files into model context through li
It ports the `pi-rules` rule injector to Codex:

- `SessionStart` and `UserPromptSubmit` load static project instructions once per session.
- `PostToolUse` watches file reads and edits, then injects matching file-specific rules.
- `PostToolUse` watches supported file reads, edits, `apply_patch`, MCP filesystem payloads, and shell command file references, then injects matching file-specific rules as additional context.
- `PostCompact` clears the per-session injection cache after manual or automatic compaction so relevant rules can be reintroduced into the compacted conversation.
- Session-level deduplication prevents the same rule from being repeated after it has been injected.

`PostToolUse` output is context-only: it emits `hookSpecificOutput.additionalContext` and does not rewrite tool output.

The runtime has no npm production dependencies, so a clean Codex marketplace copy can run without a follow-up `npm install`.

## Rule Sources

Project-level sources:
Expand Down Expand Up @@ -44,7 +49,7 @@ codex plugin marketplace add /Users/yeongyu/local-workspaces/codex-plugins
node /Users/yeongyu/local-workspaces/codex-plugins/scripts/install-local.mjs /Users/yeongyu/local-workspaces/codex-plugins
```

The local installer builds the plugin, copies a clean cache entry to:
The local installer builds the plugin and copies a clean cache entry to:

```text
~/.codex/plugins/cache/code-yeongyu-codex-plugins/codex-rules/0.1.0
Expand All @@ -55,6 +60,7 @@ It also enables:
```toml
[features]
plugins = true
plugin_hooks = true

[plugins."codex-rules@code-yeongyu-codex-plugins"]
enabled = true
Expand All @@ -80,6 +86,7 @@ For migration from `pi-rules`, equivalent `PI_RULES_*` variables are accepted as
npm install
npm test
npm run check
npm run typecheck
npm pack --dry-run
```

Expand Down
25 changes: 22 additions & 3 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "off",
"noDefaultExport": "error",
"noEnum": "error",
"noNonNullAssertion": "error",
"useImportType": "error",
"useConst": "error",
"useNodejsImportProtocol": "off"
},
"complexity": {
"useLiteralKeys": "off"
},
"suspicious": {
"noExplicitAny": "error",
"noTsIgnore": "error",
"noControlCharactersInRegex": "off",
"noEmptyInterface": "off"
}
Expand All @@ -24,6 +31,18 @@
"lineWidth": 120
},
"files": {
"includes": ["src/**/*.ts", "test/**/*.ts", "!**/node_modules/**/*", "!**/dist/**/*"]
}
"includes": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts", "!**/node_modules/**/*", "!**/dist/**/*"]
},
"overrides": [
{
"includes": ["vitest.config.ts"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
}
}
}
}
]
}
1 change: 0 additions & 1 deletion dist/cli.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=cli.d.ts.map
1 change: 0 additions & 1 deletion dist/cli.d.ts.map

This file was deleted.

91 changes: 81 additions & 10 deletions dist/cli.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { stdin as processStdin, stdout as processStdout } from "node:process";
import { runPostToolUseHook, runSessionStartHook, runUserPromptSubmitHook, } from "./codex-hook.js";
import { runPostCompactHook, runPostToolUseHook, runSessionStartHook, runUserPromptSubmitHook, } from "./codex-hook.js";
const command = process.argv[2];
const subcommand = process.argv[3];
if (command === "hook" && subcommand === "session-start") {
Expand All @@ -12,25 +12,97 @@ else if (command === "hook" && subcommand === "user-prompt-submit") {
else if (command === "hook" && subcommand === "post-tool-use") {
await runHookCli("PostToolUse");
}
else if (command === "hook" && subcommand === "post-compact") {
await runHookCli("PostCompact");
}
else {
process.stderr.write("Usage: codex-rules hook [session-start|user-prompt-submit|post-tool-use]\n");
process.stderr.write("Usage: codex-rules hook [session-start|user-prompt-submit|post-tool-use|post-compact]\n");
process.exitCode = 1;
}
async function runHookCli(eventName) {
const raw = await readStdin();
if (raw.trim().length === 0)
return;
const parsed = JSON.parse(raw);
const options = { pluginDataRoot: process.env.PLUGIN_DATA };
const output = eventName === "SessionStart"
? await runSessionStartHook(parsed, options)
: eventName === "UserPromptSubmit"
? await runUserPromptSubmitHook(parsed, options)
: await runPostToolUseHook(parsed, options);
const parsed = parseHookInput(raw);
if (!parsed)
return;
const pluginDataRoot = process.env["PLUGIN_DATA"];
const options = pluginDataRoot === undefined ? {} : { pluginDataRoot };
const output = await runHook(eventName, parsed, options);
if (output.length > 0) {
processStdout.write(output);
}
}
async function runHook(eventName, parsed, options) {
switch (eventName) {
case "SessionStart":
return isCodexSessionStartInput(parsed) ? await runSessionStartHook(parsed, options) : "";
case "UserPromptSubmit":
return isCodexUserPromptSubmitInput(parsed) ? await runUserPromptSubmitHook(parsed, options) : "";
case "PostToolUse":
return isCodexPostToolUseInput(parsed) ? await runPostToolUseHook(parsed, options) : "";
case "PostCompact":
return isCodexPostCompactInput(parsed) ? await runPostCompactHook(parsed, options) : "";
}
}
function parseHookInput(raw) {
try {
const parsed = JSON.parse(raw);
return parsed;
}
catch {
return undefined;
}
}
function isCodexSessionStartInput(value) {
return (isRecord(value) &&
value["hook_event_name"] === "SessionStart" &&
typeof value["session_id"] === "string" &&
isStringOrNull(value["transcript_path"]) &&
typeof value["cwd"] === "string" &&
typeof value["model"] === "string" &&
typeof value["permission_mode"] === "string" &&
typeof value["source"] === "string");
}
function isCodexUserPromptSubmitInput(value) {
return (isRecord(value) &&
value["hook_event_name"] === "UserPromptSubmit" &&
typeof value["session_id"] === "string" &&
typeof value["turn_id"] === "string" &&
isStringOrNull(value["transcript_path"]) &&
typeof value["cwd"] === "string" &&
typeof value["model"] === "string" &&
typeof value["permission_mode"] === "string" &&
typeof value["prompt"] === "string");
}
function isCodexPostToolUseInput(value) {
return (isRecord(value) &&
value["hook_event_name"] === "PostToolUse" &&
typeof value["session_id"] === "string" &&
typeof value["turn_id"] === "string" &&
isStringOrNull(value["transcript_path"]) &&
typeof value["cwd"] === "string" &&
typeof value["model"] === "string" &&
typeof value["permission_mode"] === "string" &&
typeof value["tool_name"] === "string" &&
typeof value["tool_use_id"] === "string");
}
function isCodexPostCompactInput(value) {
return (isRecord(value) &&
value["hook_event_name"] === "PostCompact" &&
typeof value["session_id"] === "string" &&
typeof value["turn_id"] === "string" &&
isStringOrNull(value["transcript_path"]) &&
typeof value["cwd"] === "string" &&
typeof value["model"] === "string" &&
(value["trigger"] === "manual" || value["trigger"] === "auto"));
}
function isStringOrNull(value) {
return typeof value === "string" || value === null;
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readStdin() {
return new Promise((resolve, reject) => {
let data = "";
Expand All @@ -44,4 +116,3 @@ function readStdin() {
});
});
}
//# sourceMappingURL=cli.js.map
1 change: 0 additions & 1 deletion dist/cli.js.map

This file was deleted.

11 changes: 10 additions & 1 deletion dist/codex-hook.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@ export type CodexPostToolUseInput = {
tool_response: unknown;
tool_use_id: string;
};
export type CodexPostCompactInput = {
session_id: string;
turn_id: string;
transcript_path: string | null;
cwd: string;
hook_event_name: "PostCompact";
model: string;
trigger: "manual" | "auto";
};
export interface CodexRulesHookOptions {
env?: NodeJS.ProcessEnv;
pluginDataRoot?: string;
}
export declare function runSessionStartHook(input: CodexSessionStartInput, options?: CodexRulesHookOptions): Promise<string>;
export declare function runPostCompactHook(input: CodexPostCompactInput, options?: CodexRulesHookOptions): Promise<string>;
export declare function runUserPromptSubmitHook(input: CodexUserPromptSubmitInput, options?: CodexRulesHookOptions): Promise<string>;
export declare function runPostToolUseHook(input: CodexPostToolUseInput, options?: CodexRulesHookOptions): Promise<string>;
//# sourceMappingURL=codex-hook.d.ts.map
1 change: 0 additions & 1 deletion dist/codex-hook.d.ts.map

This file was deleted.

Loading
Loading