Use when a project needs a memory server — search project and shared memory first, write durable facts with source paths, watch a docs directory, or promote facts across projects.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Is that skill safe to install?
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
Are you the author of Ai Raccoon Memory?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/arasz-ai-raccoon-memory)
---
name: ai-raccoon-memory
description: >-
Use when a project needs a memory server — search project and shared memory first, write
durable facts with source paths, watch a docs directory, or promote facts across projects.
version: 0.2.0
author: ai-badger
license: MIT
platforms: [linux, macos, windows]
scope: default
metadata:
hermes:
tags: [memory, retrieval, semantic-search, persistence]
related_skills: [mcp-index, hermes-mcp-setup]
---
# AiRaccoon Memory
## When NOT to Use
- A one-off lookup ("have we seen X before?") — run `query` and be done, no watch ritual, no write-back
- No docs directory to watch and no durable fact to write — the ritual adds ceremony, not value
- The memory-grade hook when you only need one answer — it is opt-in by env var; don't enable it for a single search
## 1. Watch-on-docs ritual (do this first)
On session start, run `get_watch_status(projectId)` for this project. If the docs directory
is not in the watched list, run `add_watch(projectId, <absolute path to docs>)` to mirror
it into memory. The watch starts `scanning` and settles to `healthy`; an already-watched path is
a no-op.
**CLI prerequisite (only when the watch errors):** `watching-disabled` or `path-outside-scope`
means the one-time per-install setup is missing (quote the `*` so the shell does not expand it):
`ai-raccoon watch scope add '<project-id|*>' <path>`, then
`ai-raccoon watch enable '<project-id|*>' true`. If the `memory_watch_*` tools are not listed at
all (older tool build on another machine), update the tool: `dotnet tool update -g arasz.ai-raccoon`.
## 2. Search-first workflow
Always pass `projectId` and `sessionId`. Pass your session id on every search. The server rejects a blank one and stores the value verbatim on the search row. Before web search, code search, or asking the user, run `query(projectId, sessionId, scope=all)` with 2-3 formulations. Try the exact phrase first, then keywords, then a plain restatement. Entries carry source paths. Cite them as evidence. Every reply carries `meta.correlationId`. Keep that id. You need it to grade the search or to record that you opened one of its files.
## 3. Escalation by result
- Decisive hit → use it; cite the source path.
- Partial hit → one targeted external search, then reconcile.
- No hit → search externally, then write the finding back with `add_memory` (source path included).
## 4. Write discipline
Durable facts only, one per entry, source included. Plain writes land in committed project
memory (`project:<id>`). For in-progress notes use workspace isolation:
`initialize_workspace` → `get_workspace_status` → `close_workspace(keep=[...])`
(or `["all"]` to promote everything; `discard_workspace` to drop). Promote durable
cross-project facts with `share_memory` — never automatically. `trigger_memory_cleanup` removes old
low-rated entries; shared entries are exempt.
## 5. Scopes and kinds
`scope=all` (default: shared + project), `scope=project`, `scope=shared` (the promotion tier only). Kind defaults to `both`. It runs the memory and code hybrids separately and returns both sections. `kind=memory` searches memory only. `kind=code` searches code only. Recording follows the request. Memory and both store the memory leg count and files. Code stores the code count with an empty file list. Code paths never enter the table.
## 6. Gotchas
- `add_memory` has **no `path` param** — the entry path is derived from its content.
- **Never pass `context`** unless workspace isolation is intended: it silently sets
`scope='custom'`, invisible to project-scoped search.
- `embed_deferred_entries`: omit `limit` to process all pending entries.
- `delete_context` requires full access mode.
- When you open a file the search returned, call `set_memory_usage_record` with the same `correlationId`, the file path, and `servedRank` when you saw one. Rank is 1-based. Under `kind=both` a bare rank cannot name its section. That ambiguity is intentional, so send no section qualifier. Grade with `set_query_result_grade` (1-5) on the same id when you have a judgment.
- A `query` result (`kind=code` or `kind=both`) carrying a warning starting
`code engine not configured` means its code section is FTS5 keyword matches only — semantic
code hits are missing, not absent; don't read it as a complete answer. Tell the user, once
per session, verbatim: run `ai-raccoon model set code default` (downloads and activates the
default code embedding model, ~187 MB, one-off) — don't repeat the hint on later searches in
the same session. Don't re-run the search hoping for vector results: nothing changes until
that command runs.
## 7. Bulk ops
`ingest_file` / `ingest_directory` bulk-load files; `get_store_statistics` reports bank
size; `sync_to_cloud` exchanges snapshots with cloud storage when configured.
## 8. Per-prompt memory context (automatic)
This skill ships a hook that searches memory for you before the model sees each prompt. It
runs on Claude Code and Copilot CLI (`UserPromptSubmit` / `userPromptSubmitted`, entry
`scripts/memory_context_hook.py`) and in Hermes CLI sessions (`pre_llm_call`). It never runs
on pi. It is POSIX only: on Windows it stays silent and spawns nothing.
**When it fires.** Every prompt that passes pi's `shouldEnrich` gate (long enough, enough
distinct words, not a slash command or a control word like `continue`), in a project where
`.ai-badger/project-id` resolves and the `ai-raccoon` executable is found (`PATH` first, then
`~/.dotnet/tools/ai-raccoon`). A turn the harness wrote rather than a person (one that starts
with a marker in `INJECTED_PREFIXES`, such as `<task-notification>`, or holds only
`<system-reminder>` blocks) is skipped before any search or spawn. Leading reminder blocks in
front of your own text are stripped and the rest is gated as usual; a question whose first line
is a pasted notification is skipped for that turn. Each run spawns the bare `ai-raccoon` binary as a proxy, once,
and sends every search over that one session. The hook never reads the ai-raccoon token; the
proxy does the identity proof. If no serve is running, the proxy may start one, and that serve
outlives the hook under its own idle watchdog. When hits survive pruning, the hook injects a
"Memory context" block identical to pi's `toMemoryContext` output. Otherwise it injects
nothing, and the hook always exits 0. An expected failure (no proxy, a timeout, an error
reply) is silent. A missing or broken `memory_context.py`, or a defect in the hook's own
code, leaves one line in `~/.ai-badger/hook-errors.log` naming the exception type and where it
was raised, never the prompt; under Hermes the same line goes to Hermes's log as a warning.
If you opted in to third-party egress and a `dataPolicy` lock refuses it, one line naming the
locking config is written there too.
**Two modes.** Both inject the same block.
- *Pipeline*: runs only when you opt in — `AI_BADGER_ALLOW_THIRD_PARTY=1`, `OPENROUTER_API_KEY`
set, `AI_BADGER_MEMORY_CONTEXT_PIPELINE` not `"0"`, and no ancestor `.ai-badger/config.json`
declaring `dataPolicy` (a key alone does nothing). An OpenRouter model plans 2 to 6 retrieval queries, each is searched once, the
Jev decisions endpoint scores the pooled hits, and pi's document-aware merge keeps the best
five. The run's total is 90 s on Claude and Copilot, split into pi's stage limits
(planner 15 s, each search 15 s, Jev 8 s). Under Hermes the total is 25 s and the stage
limits shrink with it, so a planner that uses its whole share still leaves time for two
full searches and the Jev stage. If the planner fails, the run falls back to one search on
the prompt.
- *Single search*: every other case. One search on the prompt, capped at 5 s. Nothing leaves
the machine except the call to the local proxy.
**What leaves the machine.** Only in pipeline mode, and only to OpenRouter: the gated prompt
(the planner's input, and its first 32 000 characters in each Jev request), and for up to 48
pooled hits their file path, kind and a 500-character excerpt of memory *and source code*.
The key is read from the environment only and is never logged or written anywhere. Without
the opt-in everything but the ai-raccoon search stays local; a project that commits
`"dataPolicy": "local-only"` keeps it local whatever a developer's shell sets (its object form
can allow named company hosts, still only with the opt-in). An exported `AI_BADGER_PROJECT_ID`
that differs from the working directory's project id keeps the single local search. The full
inventory is `docs/reference/data-access.md` in the ai-badger repository.
**Switches.** Only the literal `"0"` counts for the three off switches, and only `"1"` for the opt-in.
| Variable | Effect |
|---|---|
| `AI_BADGER_MEMORY_CONTEXT=0` | Hook off: no spawn, no HTTP, no block |
| `AI_BADGER_MEMORY_CONTEXT_PIPELINE=0` | Single search even with a key; no HTTP |
| `AI_BADGER_MEMORY_CONTEXT_PLANNER_MODEL` | Planner OpenRouter `vendor/model` override (optional `openrouter/` prefix; native paths and punctuation are refused) |
| `AI_BADGER_ALLOW_THIRD_PARTY=1` | Opts in to third-party egress (pipeline mode); must be exactly `"1"`, and a `dataPolicy` lock overrides it |
| `OPENROUTER_API_KEY` | The pipeline's credential; does nothing without the opt-in |
| `AI_BADGER_PROJECT_ID` | Overrides `.ai-badger/project-id`; exported globally, it routes every repo to one project |
| `AI_BADGER_RACCOON_REGISTER=0` | Scaffold and den-refresh make no `project id` lookup or register call: the id is still minted locally, but nothing is asked of the bank |
Without the override, the planner uses the `medium` tier's preferred model through the `task`
skill's `model_groups.py` and the project's `model-groups.json`. A project that declined the
`task` skill has no resolver, so the planner has no model and every run falls back to a single
search unless the override is set. The planner model comes from project data and is billed to
your key; the override pins it.
The memory-first gate (the PreToolUse decision that precedes a text search) names the same id the
hook reads: the nearest `.ai-badger/project-id`, with `AI_BADGER_PROJECT_ID` first. The old
raccoon-side override is retired (see ADR-0036 in the ai-badger repository), so the decision and
the search it demands cannot point at two different projects.
**Repo text search: memory first.** The gate keys on the search pattern. A grep/egrep/fgrep/
rg/find anywhere in a shell command (every segment of `a; b | c` counts, a piped search
included), or a Grep/Glob/search_files call, passes only when every pattern was already
memory-searched for this project. This includes `rg`.
On a pattern's first sighting the gate runs one `query` and hands the result to the
agent. On a host whose tool-call seam carries model-side context (Claude Code, Codex and pi)
the search is **allowed** and the memory block arrives as `additionalContext`: the agent gets
the data and the user sees nothing. A host without that seam (Copilot CLI, Hermes CLI) gets a
one-line denial — "search memory for … first, then re-run this search" — and the agent runs
the search itself, so the data is the same either way.
The agent receives a JSON object with `textSearchInput`, an array of exact
`{pattern, rawResultHash}` pairs: one entry per searched pattern, and a `rawResultHash` only
for a pattern whose raw output the gate captured. A `find` call has patterns but no capture,
so its entry carries the pattern alone. The same compact object is appended as
one JSONL line to `~/.ai-badger/memory-first-search/searches.jsonl` (a private rolling log
bounded to 1 MiB), and the pattern is recorded.
For shell grep-family commands, the hook also starts the original command asynchronously and
saves its raw output in the ai-badger audit database for about two minutes. When raw output is
needed, use the `get-raw-text-search` skill or run its script with `--pattern-hash <hash>`;
a known pattern can also be fetched with `--pattern <pattern>`. The result is JSON and may be
`pending` if capture has not finished yet. After reading the memory hits, re-issue the search
only if it is still needed. When the hook could not search (no
`ai-raccoon`, timeout), it records nothing and asks you to run `query` with that
pattern as the query; your search's query is recorded, and a pattern that is a whole-word run
of it counts as searched (comparison ignores case, `^ $ \b`, grouping parentheses and regex
escapes such as `\.`). Searched patterns persist per project and are never reset.
**Excluded list.** A grep/egrep/fgrep/rg reading a pipe from git, gh, ps, ls, env, printenv,
history, docker, kubectl, pip, npm, lsof, brew, dotnet is not repo search:
`git log | grep fix` is excluded from repo search, including through `sudo` wrappers.
Only the program immediately before the grep counts (`git log | head -50 | grep fix` is gated:
`head` is not listed). A grep with a file operand, `-r`/`-R`/`-d recurse`, `rg --files`, an
input redirect, or run by `xargs` stays gated, as does any file grep elsewhere in the command.
Exception: a listed program that prints repo files is not exempt — `git ls-files`, `git ls-tree`, `git grep` (subcommand found past `-C`/`-c`/`--git-dir`/`--work-tree`) and `ls -R`/`--recursive`; a grep over their output stays gated.
A `query` credits its query only when the call succeeded (not Claude's `PostToolUseFailure`, a Copilot `resultType` other than `success`, or a Hermes status other than ok) and its `projectId` equals the project the gate resolved; a search of another project, or one with no `projectId`, unlocks nothing. A `grep -f`/`rg -f`/`--file` search takes its patterns from a file the gate cannot read, so it is keyed on its own words.
**Search log.** Each enriched prompt, and each planned query, is a real `query` and
lands in ai-raccoon's search log like any other search.
**Per agent.**
- *Claude Code*: prints the `hookSpecificOutput` envelope. The hook's `timeout` is 100 s,
because `UserPromptSubmit` otherwise defaults to 30 s.
- *Copilot CLI*: prints flat `{"additionalContext": ...}`, the only shape Copilot CLI 1.0.88
consumed when measured (the envelope was ignored). `timeoutSec` is 100. Copilot loads repo
hooks only for a folder listed in `trustedFolders` in `~/.copilot/config.json`; in an
untrusted folder the hook never runs, which looks exactly like an empty result.
- *Hermes*: CLI sessions only. The arm runs when `(platform or "cli") == "cli"`, Hermes's own
normalisation, so a gateway session (Telegram, Discord and the rest) never triggers a search
or OpenRouter call. It also needs `.ai-badger/skills/ai-raccoon-memory/` in the project. The
block goes last in the injected context, followed by `(end of memory context)`. Hermes calls
`pre_llm_call` once per user turn, before its tool loop, so a tool loop searches once.
Hermes abandons a `pre_llm_call` callback after 30 s by default and then drops that turn's
whole injection, including a message-bus delivery the callback already consumed. The 25 s
limits exist to keep a slow run inside that window; a turn that overruns it anyway loses
everything the callback would have added.
Declining this skill through `config.exclude` removes the Claude wiring and turns the Hermes
arm off. The Copilot hooks file still names the command today; with the skill's scripts absent,
its existence guard answers each prompt with a "hook skipped" system message instead.
**Parity with pi.** The block is byte-identical to pi's for any hit whose path and string rank
hold no line-break or tab characters, whose snippets hold no U+0085, whose rank is a number,
string, boolean or null, and whose text is not cut inside a non-BMP character. Outside that,
this port collapses line breaks in paths and ranks to one space (pi keeps them raw), prints `?`
for a list or object rank, counts truncation in code points, and cuts a path at 300 and a rank
at 32 characters. The "Parity with pi" section of ADR-0031 lists every divergence.
## 9. Decisions
ai-raccoon also keeps versioned decision records for each task. Every write takes four ids:
- `projectId`: the same id you use for memory.
- `taskId`: the `/task` id from the task tracker.
- `sessionId`: your own session id.
- `requestId`: a fresh UUID for each command. Send the same requestId again only to replay that
command; the server then returns the first result and writes nothing new.
Write in this order. Run `register_decision_task` once, before the first decision of the task.
Then run `record_decision`. To change a decision later, use `amend_decision` (new content),
`transition_decision` (accepted, rejected or deprecated), `supersede_decision` (the only way to
make a supersedes edge) or `link_decisions` (depends_on, supports, contradicts). To read, use
`query_decisions`, `find_decision_precedents`, `get_decision_graph`, `preview_decisions` and
`export_decisions`.
```text
record_decision {"projectId": "<id>", "taskId": "aib-example", "sessionId": "<session id>",
"requestId": "<fresh UUID>", "title": "Use SQLite for the cache", "category": "architecture",
"scenario": "The cache must survive a restart", "outcome": "SQLite file per project",
"reasoning": "No server to run; the stdlib reads it"}
```
**When the reply is rejected.** In ai-raccoon 2.6.0 and earlier, the decision tools commit the
write, but a host that validates the tool output schema (Claude Code is the one known host) can
reject the reply (null fields marked required). Do not retry. Check the write with
`ai-raccoon decisions preview --project-id <id> --task-id <taskId>`. If a retry is necessary, send the same sessionId and the same requestId,
so the server replays the first result. Where MCP is not available (pi), the CLI
`ai-raccoon decisions task register | record | get | query | export | preview` is the primary path.
## 10. Verification Checklist
- [ ] `get_watch_status` shows the docs dir `healthy`
- [ ] `query(projectId, sessionId, scope=all)` returns docs-derived hits
- [ ] A durable finding was written back with `add_memory`, source path included
- [ ] Each task decision was recorded under a registered task, and `query_decisions` returns it