Codex Rules: per-session cache for additionalContext (Claude Code InstructionsLoaded parity)
Problem
A common Codex hook pattern: UserPromptSubmit reads project rules
(e.g. .claude/rules/*.md with paths: frontmatter — same convention
Claude Code uses), matches them against paths mentioned in the user's
prompt, and emits matched rule bodies ashookSpecificOutput.additionalContext.
Problem: Codex re-injects the same rule bodies on every prompt.
If a user works on apps/design/ for 20 turns, the same 5 rules
(~10 KB of additionalContext) get re-injected 20 times. That's
~200 KB of redundant context for a small project — bigger projects
easily exceed 1 MB per session.
How Claude Code solves this natively
Claude Code's InstructionsLoaded event (with load_reason:)
nested_traversal | path_glob_match | session_start | compact | include
fires only when:
- A rule file is discovered for the first time in the session
- The model touches a path that triggers a previously-unseen rule
- Cache is invalidated (compaction)
A rule loaded on turn 1 stays in context. The hook on turn 2 sees the
same paths but emits nothing new — the rule is already there.
Source: src/utils/attachments.ts:1755-1769 (memoryFilesToAttachments,
where path_glob_match fires) and src/utils/claudemd.ts:1395 (uses
the ignore library for
gitignore-semantics path matching).
Empirical impact (our setup)
We built a userland workaround: per-session cache file at~/.codex/cache/<plugin>/<session_id>.json, mtime-based invalidation,
gitignore-semantics matching via the ignore library.
| Turn | Action | additionalContext size |
|------|--------|------------------------|
| 1 (cold) | edit apps/design/foo.tsx | 10,225 chars (full rule bodies) |
| 2 (warm) | edit apps/design/bar.tsx | 356 chars (status only: "already in context") |
| 3 (touch + edit) | touch .claude/rules/x.md && edit apps/design/baz.tsx | ~2 KB (re-injects only the changed rule) |
~96.5% context reduction on the warm path. A 20-turn session in the
same domain saves ~200 KB.
Proposed approaches (4 — pick what fits the team's roadmap)
- Native event (heaviest): add
InstructionsLoaded-equivalent
that hooks can subscribe to with built-in path-glob matching
against frontmatter. Fully reactive (fires on tool path access).
Mirrors Claude Code 1:1.
- Helper library (medium): publish a Codex helper that hooks
call to compute "what's new since last turn" given session_id
- candidate set. No core changes; pattern becomes documented.
- Cookbook docs (lightest): publish the pattern as official
guidance in docs/hooks/ — "hooks that inject state should key
cache by session_id and dedupe against prior turns."
- Marketplace plugin (zero core changes): ship our reference
implementation as an official curated plugin. Users install once
via codex plugin marketplace add; works for any project with a
rule directory.
Reference implementation (open to contribute)
~530 lines of Node, MIT-able. Behaviors covered:
- gitignore-semantics matching via
ignorelib (Claude Code parity) - Brace expansion in
paths:frontmatter ({ts,tsx}) @-mention extraction (priority candidate)- Conversational keyword extraction (top-level dir mentions —
necessary because Codex hooks fire BEFORE tool decisions, so we
can't be purely reactive like Claude Code)
- Session-scoped JSON cache with mtime invalidation
- Atomic writes, 30-day TTL pruning, cache failure is non-fatal
Happy to extract a clean version under whichever path the maintainers
prefer (1/2/3/4).
Adjacent issues
- #16430 (plugin-local hooks not firing in 0.128.0) — currently
blocks distributing this as a plugin; registering globally as
workaround. Confirmed reproducible in 0.128.0 release.
- #19385 (
PreToolUseadditionalContextnot supported) — would
enable fully reactive loading like Claude Code's path_glob_match.
- #20766 (TUI: collapse hook context) — UI-side mitigation but
doesn't reduce the token cost.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗