Docs/Integration

Integration

How ROSE attaches to Claude Code and Codex, and what it writes where.

How ROSE attaches to Claude Code and Codex, and what it writes where.

Claude Code#

/plugin marketplace add yiheinchai/rose
/plugin install rose@rose

The plugin ships the hooks, the rose skill and the /rose command. Hook commands resolve through ${CLAUDE_PLUGIN_ROOT}/bin/rose, so nothing needs to be on your PATH and no Python environment is required.

By hand#

rose install --target claude                 # this repo only
rose install --target claude --scope user    # every repo
rose install --target claude --dry-run       # show what it would write

This edits .claude/settings.json (or ~/.claude/settings.json) additively. Each entry ROSE adds is tagged "_rose": true, and rose uninstall removes only tagged entries — hooks you configured yourself are left alone.

Hooks used#

EventCommandPurposeBudget
UserPromptSubmitrose hook user-prompt-submitserve the lessons that bear on this prompt30s; instant while the store fits the budget
Stoprose hook stopafter a substantial turn, reflect — off-thread by default15s, nothing inline
SessionEndrose hook session-endparse the transcript, then detach the whole learner30s; returns in ~0.1s, always

UserPromptSubmit returns:

{"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",
                        "additionalContext": "## Recalled lessons (ROSE) …"}}

Injected text is explicitly framed as prior knowledge rather than user instruction, so a stale lesson cannot impersonate a request.

Seeing it happen#

An injection is never silent. While the hook runs you see its status line, and when it injects, the hook returns a systemMessage that Claude Code shows you:

⋯ Recalling lessons…
⋯ ROSE · recalled 2 lessons (312 tok): retry, k8s-deploys   +1 patch

Unresolved contradictions are flagged there too. For the full picture — what was offered to the model, what it decided and why, and the exact text injected — run rose trace --prompt "...".

Recall costs a model call on the hot path **only once the store outgrows max_pack_tokens**. Below that everything is served unfiltered, with no call at all, in milliseconds — there is nothing to choose between. That is deliberate — injecting the wrong lesson is worse than injecting none, and only a reader can tell the difference — but it is a real latency cost, so it is cached by prompt and can be switched off with rose config recall.enabled false.

Where reflection runs#

learning.nudge_mode picks the trade:

ModeContext the reflector seesCostInterrupts you
background (default)a transcript digest, ~3k tokensone small callno
forkthe whole session, inherited~0.1× its tokens via cache readsno
blockthe agent's own live contextnone extrayes — one turn
offnone until session endno

fork spawns claude --resume <id> --fork-session detached. --fork-session allocates a new session id, so the live session is never written to. It is affordable because prompt-cache reads bill at 0.1× and the cache keys on prefix content rather than session identity — the fork hits what the live session just wrote. Claude Code uses a 1-hour cache TTL, which covers the reflection cooldown comfortably.

It is not the default because 10% of a very large context still exceeds a 3k digest, and the digest has proven able to identify conceptual corrections rather than only mechanical ones. Pick fork when fidelity matters more than tokens — the digest necessarily drops nuance, and a conceptual mistake can live in the nuance.

Every spawned reflector runs with ROSE_CHILD=1; without it the fork fires these same hooks and forks itself, forever.

Overlapping reflectors#

Reflectors can overlap — a slow one can still be running when the next fires. They are not prevented from doing so, because two reflectors looking at different windows may legitimately find different things.

What is prevented is recording the same lesson twice, and the defence is reconciliation, not scheduling. Every capture is checked against what is already known and a duplicate is stored as nothing. A minimum time apart would not achieve this: whatever gap you pick, a reflector that outlives it reopens the race, and two reflectors an hour apart can still reach the same conclusion.

The one thing scheduling cannot fix is a read-decide-write race: if both reflectors read the store before either writes, both conclude "new". So rose add holds a write lock across decide-and-write, and a writer waits for it rather than skipping — losing the lock and giving up would silently drop a lesson. Stale locks (from a killed reflector) expire rather than wedging the store.

rose absorb additionally takes a lock of its own and skips if another absorb holds it, since two digest passes over the same transcript would only duplicate work.

Failure behaviour#

Every hook path is wrapped and returns exit 0. A broken store, an unparseable transcript or a raised exception all degrade to "inject nothing" rather than interrupting the session. This is deliberate: a memory system that can break someone's editor will be uninstalled, and correctly so.

SessionEnd gets a stricter rule still. The host is shutting down and will cancel a hook that is still running, so slow work there is not late — it never happens. Judging a session takes a model call, so the hook does none of it: it parses the transcript, decides whether the session is even worth learning from, and hands everything else to a detached rose absorb. It returns in about 0.1s.

absorb runs judge → learn → compress in that order, in one process. They were briefly three parallel spawns, which raced: compaction is only eligible once the judgement has recorded the successes that make a node due, so it usually lost.

Codex#

Codex's hook schema is less settled than Claude Code's, so the reliable route is an instruction block appended to AGENTS.md:

rose install --target codex

which appends a marked block telling the agent to run rose recall --prompt "…" before non-trivial work. If ~/.codex/hooks.json already exists, a UserPromptSubmit entry is added there as well.

Codex also works as an execution backend regardless of how recall is wired — it is often the better choice for background compaction, since `codex exec --ephemeral --output-schema` gives native structured output:

rose config agent codex
rose compact --due --agent codex

What gets written#

.rose/
  config.yaml        settings (see `rose config`)
  nodes/<family>/    lesson nodes — the tree. Worth committing.
  episodes/          the replay corpus. Worth committing.
  sessions/          per-session scratch. Machine-local.
  events.jsonl       telemetry. Machine-local.
  judge-cache.json   cached judgements. Machine-local.
  background.log     output from detached learning runs (`rose absorb`).

Two scopes#

If ~/.rose exists it is layered underneath the project store. Lessons from both are recalled; new ones are written to the project. This is how a cross-project principle ("prefer the model's judgement over a similarity score") and a repo-specific fact ("this suite needs PAYMENTS_PG_PORT") each live at the right scope instead of one being in the wrong place.

rose init ~                      # create the global store
ROSE_HOME=~/.rose rose add "..."   # teach it something that follows you everywhere

Editing a global lesson from inside a repo writes back to the global store rather than forking a local copy that then drifts.

.rose/.gitignore excludes sessions/ and events.jsonl by default, so committing .rose/ shares the lessons and their regression corpus with your team without the machine-local noise.

Privacy#

Everything written to disk passes through redact.py first: API keys, tokens, private keys, card numbers and secret=… assignments are replaced with [REDACTED], and emails are reduced to their domain. It is deliberately biased toward over-redaction — a mangled lesson is recoverable, a leaked key is not.

ROSE never sends anything anywhere. Model calls go to whichever CLI you already have configured, with your existing credentials.

Turning it off#

rose config recall.enabled false       # stop injecting
rose config compaction.enabled false   # stop compressing
rose config learning.enabled false     # stop minting lessons
rose uninstall --target claude         # remove the hooks entirely

ROSE_DISABLE=1 in the environment disables everything for a single run, which is also how spawned child agents avoid recursing.