Local rule memory for Claude Code and Cursor—corrections distilled into lasting guidance, recalled in context, one deliberate gate when stakes are high. 经验留下的痕迹,比记忆更深。
Project description
Nokori (残り)
What experience leaves behind runs deeper than memory.
A rule notebook for Claude Code and Cursor — turns your corrections into rules that come back automatically next time.
Nokori does not save “what you chatted about”; it saves “what to do next time”: when your message looks similar, matching rules are injected into context. For high-risk corrections, it can block the first tool call once so the agent reads the rule before editing files or running commands.
Who is it for?
- People who keep correcting the same kinds of mistakes (force push, forgotten migrations, dangerous commands)
- People who want cross-project “don’t do that” knowledge instead of starting over in every repo
- People who accept “rules live in local SQLite and can be exported,” and do not want to resend entire chats to an LLM
One minute overview
You correct Claude
→ Nokori records a rule (trigger scenario + what to do)
→ Next time your message resembles that moment
→ Automatically written into Claude’s context (reminder)
→ For high-risk correction types with a strong match: block once before the first file edit or command (Gate)
During chat, Nokori stays fast (retrieval + files, no LLM); after the session ends, an LLM mines new rules from the transcript (session log).
Glossary
If you hit English abbreviations on first read, skim this table first; key concepts are repeated later.
| Term | Meaning |
|---|---|
| hook | A small command Claude Code runs automatically at fixed moments (e.g. before/after each message) |
| injection | Writing matched rules into the context Claude sees for the current turn |
| Gate | For a few “high-risk correction” rules: deny the first matching tool call once, forcing Claude to read the rule |
| marker | A temporary “read Gate rules first” flag for the current turn; cleared after one use |
| transcript | Claude’s full-session .jsonl log; read when extracting rules automatically |
| trigger / action | The two halves of a rule: “under what situation” + “what to do” |
| short_id | A rule’s short ID (e.g. a3f2b1), used to dismiss or cross-reference |
| dismiss | Retire a rule (no longer retrieved, no longer gated) |
| HOT / WARM | Match tiers: highly relevant / somewhat relevant; hotter tiers get more text |
| BM25 | Keyword-overlap scoring; zero GPU, enabled by default |
| embedding | Semantic similarity scoring; optional once you have enough rules |
| RRF | Algorithm that merges BM25 and vector rankings into one list |
| fail-open | When Nokori itself errors, it does not block Claude — it skips reminders for that turn |
| extract | Use an LLM to extract candidate rules from a transcript (cold path, not urgent) |
| shadow pool | Rules from other projects: used only to decide whether to promote to global; not injected into your current chat |
| promotion | After a project rule is validated across multiple other projects, it becomes global (visible everywhere) |
| candidate / active / dormant | Pending confirmation → in use → dormant after long disuse |
| merged / archived | Superseded by a newer rule / dismissed by you or the system |
| supersede | A new rule replaces an old one (old status becomes merged) |
| OpenAI-compatible | Point the API at .../v1 to use Ollama, LM Studio, OpenRouter, etc. |
How it works
Nokori registers 4 hooks in Claude Code. During normal chat they only query the local DB, score, and read/write small files — no LLM calls in hooks (otherwise every message would wait on a model).
| Hook | In plain terms | Latency budget |
|---|---|---|
SessionStart |
Session start: optionally inject unextracted user snippets from the previous session + trigger DB maintenance | ≤ 1.5s |
UserPromptSubmit |
Each message sent: retrieve rules → inject context → write Gate marker if needed | ≤ 500ms |
PreToolUse |
Before a tool call: if a marker exists, block once, then clear the marker | ≤ 50ms |
SessionEnd |
Session close: write a pending extract job; in async mode may run extract in the background | ≤ 200ms |
Two core behaviors:
- Reminder (injection) — matched rules are written into
additionalContextby HOT/WARM tier so Claude sees them before replying - Block once (Gate) — only correction / anti_pattern rules that match accurately, with high confidence, and are active block tools; solution rules remind only, never block (see Injection vs blocking)
Installation
git clone https://github.com/KorenKrita/nokori.git
cd nokori
pip install -e .
# Optional: local embedding (installs sentence-transformers and downloads model weights to ~/.nokori/models/)
pip install -e ".[local-embed]"
# Register hooks (default: Claude Code only; with [local-embed], also prefetches weights)
nokori install # Claude Code → ~/.claude/settings.json
nokori install --cursor # Cursor only → ~/.cursor/hooks.json
nokori install --all # both (prints duplicate-hook warning)
# Skip weight download: nokori install --no-prefetch-embed
# Manual download/retry: nokori embed prefetch
# Verify
nokori health
nokori status
nokori logs # hook / pipeline / async-extract logs
nokori install writes the hooks above into ~/.claude/settings.json, merging with your existing plugins rather than overwriting them. If settings.json is corrupted (invalid JSON), install refuses to write and exits (same validation as nokori health for settings).
Hook commands use python -I -m nokori hook (-I = isolated: ignores PYTHONPATH and the current directory so a repo-local nokori/ folder cannot shadow the installed package). Use a normal install (pip install nokori or pip install -e .); do not rely on PYTHONPATH alone to load Nokori in hooks.
# Preview changes before writing
nokori install --dry-run
# Uninstall (removes only nokori hooks, keeps others)
nokori install --uninstall
# Temporarily disable (hooks remain but do not run)
nokori install --disable
nokori install --enable
Claude Code and Cursor
Nokori supports Claude Code (default install) and Cursor (native hooks or import from Claude). Use one registration path per machine — not two at once (see below).
Which install command?
| You want | Command |
|---|---|
| Claude Code only | nokori install |
Cursor only (native ~/.cursor/hooks.json) |
nokori install --cursor |
| Both platforms | nokori install --all (prints a warning) |
nokori install --disable / --enable only change Claude’s settings.json. To stop Cursor: nokori install --uninstall --cursor.
Pick one way to hook Cursor (do not combine)
| Path | What you do | Good when |
|---|---|---|
| A — Import from Claude (simplest) | nokori install, then in Cursor: Settings → Hooks → Import from Claude Code |
You already use Claude Code; one shared hook config |
| B — Native Cursor hooks | nokori install --cursor only; do not turn on Claude import in Cursor |
You want Cursor-only setup with Shell in the matcher and deferred inject |
If both paths are active (Claude settings + Cursor hooks.json, or import + native), the same user message can trigger Nokori twice. By default hook coalesce (NOKORI_HOOK_COALESCE=1) lets only the first invocation run retrieve/gate/extract; the second gets an empty pass-through. nokori health warns when both are registered. Still, prefer one path — see README table above.
Extra tips:
- Path A: disable project-level hooks imported from this repo’s
.claude; keep user-level~/.claudenokori only. - Path B: do not enable “Import from Claude Code” in Cursor settings.
Cursor-only details
Terminal tool name: Cursor Agent uses Shell; Claude Code uses Bash. Native install (--cursor) includes Shell in the preToolUse matcher. If you only imported Claude hooks, extend the PreToolUse matcher to include Shell (or *), or gate/deferred inject may never run on shell commands. When Nokori detects a Cursor transcript (~/.cursor/...), the in-hook gate matcher also defaults to include Shell (Gate two-layer matching).
How rules appear in Cursor: Cursor’s hook docs allow only continue and user_message on beforeSubmitPrompt — not Claude’s additionalContext. Nokori still retrieves rules on every send. Blocking uses Cursor’s permission: deny on preToolUse. Session-start hot cache uses sessionStart → additional_context. Per-message rule text is best-effort on beforeSubmitPrompt; when that hook does not run, see deferred inject below.
Deferred inject (when beforeSubmitPrompt is skipped): For a user turn where Cursor never fired beforeSubmitPrompt, the first matching preToolUse (e.g. Shell, Write) may deny once and put the full rule text in agent_message. Run the same tool again after the deny — that is expected, not a failure. Other tools in the same turn are not denied again (atomic dedup per prompt).
See nokori install --help.
Quick start
These three steps are enough to feel Nokori; details are in later sections.
1. Add a rule manually
nokori add \
--trigger "Force pushing to a shared branch" \
--action "Use --force-with-lease, or push to a new branch" \
--rationale "force push overwrites peers' work" \
--source-type correction \
--confidence high \
--variants "git push --force,git push -f" \
--terms-zh "强推,覆盖代码"
Without --project-id, the rule is written with project_scope=global (visible in the formal pool for all projects). With --project-id, project_scope=project and bound to that project_id.
2. Simulate retrieval (no Claude session required)
nokori test "I'll just git push --force this branch"
# Default project_id = current directory git root (same as hooks); override with --project
Output:
prompt "I'll just git push --force this branch"
candidates 1 rules in pool
bm25.matches 1
HOT (1):
abc123 rrf=0.0164 bm25=1.53 matched=['branch', 'force', 'git', 'push']
Force pushing to a shared branch
WARM (0):
gate.would_block True
abc123: Use --force-with-lease, or push to a new branch
3. Try it in a real session
Open Claude Code and work as usual. When your message resembles a rule:
- Claude sees injected rules before replying (HOT gets more text, WARM gets a short line)
- For correction / anti_pattern with a very strong match: the first Write / Bash / etc. may be blocked once; the UI shows the reason and
short_id - Within the same message, after one block, later tool calls proceed (marker cleared)
- Solution rules may appear in prompts but never block tools
4. Outdated rules? (Dismiss)
Each rule has a short_id (e.g. a3f2b1), shown in injection text and Gate block reasons. When a rule no longer applies, retire it (status becomes archived; no retrieval, no Gate).
Option 1: terminal (always available)
nokori dismiss a3f2b1
Option 2: say it in chat (works with Gate / injection hints)
When a rule was just injected, or Claude was blocked by Gate, the hint says you can write dismiss <short_id> to retire it. In your next user message:
dismiss a3f2b1
The UserPromptSubmit hook recognizes this and archives the rule.
| Comparison | CLI nokori dismiss |
Chat dismiss <short_id> |
|---|---|---|
| Time window | Injected within the past 24 hours (any session) | Injected within past 24 hours; normal session_id limits to current session; when session_id is -, same as CLI (any session) |
| Verb | Fixed subcommand | Configurable via dismiss_phrase (default dismiss) |
If you set dismiss_phrase to forget, write forget a3f2b1 in chat (nokori dismiss subcommand name unchanged). Format is fixed: one word + space + short_id, not free-form natural language.
Configuration: dismiss_phrase / NOKORI_DISMISS_PHRASE, see Configuration file and config.toml.example.
Gate and PreToolUse: two layers of tool matching
What is Gate? It does not disable tools for the whole session. On the first sensitive tool call in a turn, Claude must see the relevant rule first. After one block the marker is cleared; later tool calls in the same message run normally.
Many people assume there is a single “Gate blocks tools” switch. There are actually two layers, configured in different places:
Claude is about to call a tool
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Claude Code settings.json PreToolUse.matcher │
│ “Should the nokori hook pre-tool-use run?” │
│ Default: Edit|Write|MultiEdit|Bash|NotebookEdit │
│ Read / Grep etc. do not enter the hook by default │
└─────────────────────────────────────────────────────────┘
│ hook ran
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Nokori [gate].matcher (NOKORI_GATE_MATCHER) │
│ “Inside the hook, should this tool_name be blocked?” │
│ Default: same as above; Python regex, fullmatch on │
│ payload.tool_name │
└─────────────────────────────────────────────────────────┘
│ marker present and matched
▼
deny once → delete marker → retry same tool → allowed
When Gate blocks, the hook returns Claude Code’s official format (Hooks reference — PreToolUse): hookSpecificOutput.permissionDecision: "deny" and permissionDecisionReason (shown to Claude). Top-level decision/reason for that event are deprecated; Nokori no longer emits them.
Layer 1: which tools run the hook
- Config file:
~/.claude/settings.json(written bynokori install; does not readconfig.toml) - Field:
matcheron the nokori entry underhooks.PreToolUse - Default (on install):
Edit|Write|MultiEdit|Bash|NotebookEdit - Run hook on any tool: set that entry’s
matcherto*(Claude Code convention for all PreToolUse events)
Example (nokori entry only; keep your other hooks):
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "nokori hook pre-tool-use",
"timeout": 5
}
]
}
]
}
}
If already installed, edit settings manually, or nokori install --uninstall then install (writes repo defaults, not *). No config.toml change needed afterward.
Layer 2: which tool_name values actually block
- Config file:
[gate] matcherin~/.nokori/config.toml, or env varNOKORI_GATE_MATCHER - Meaning: when the hook already ran, match
tool_namein the payload with Pythonre.fullmatch - Default:
Edit|Write|MultiEdit|Bash|NotebookEdit - Block any tool that entered the hook: set to
.*(not literal*, which is invalid in regex)
[gate]
matcher = ".*"
Changing only this layer while settings still exclude Read: Read never enters the hook, so it cannot be blocked. Change both layers for “any tool may be gated.”
Injection vs blocking
Injection (additionalContext) |
Gate (PreToolUse deny) | |
|---|---|---|
| Rule scope | Formal pool HOT + WARM | Subset of formal pool HOT |
source_type |
All (including solution, preference) | correction, anti_pattern only |
| Other conditions | Retrieval tier thresholds met | Also high + active |
Example: a solution rule can appear in HOT prompts but will not Gate-block your first Write/Bash.
Other Gate-related settings
| Setting | Purpose |
|---|---|
[gate] enabled / NOKORI_GATE_ENABLED |
Master switch; off = inject only, no block |
[gate] ttl_seconds / NOKORI_GATE_TTL_SECONDS |
Marker TTL (default 600s); expired markers do not block; 0 = never expire |
Prompt-hash mismatch (fail-open): UserPromptSubmit stores the current prompt hash when writing a marker; PreToolUse resolves the current hash from the payload or the latest injections.prompt_hash for this session (not the newest marker file on disk). If unresolved or mismatched (user already sent the next message), delete the marker and allow the tool, no block.
Automatic extraction
Background work after a session ends: with LLM configured, Nokori reads Claude Code’s transcript (.jsonl session log), summarizes corrections into candidate rules, then merges with existing rules.
# Configure LLM (any OpenAI-compatible endpoint)
export NOKORI_LLM_BASE_URL="http://localhost:11434/v1"
export NOKORI_LLM_MODEL="qwen2.5:7b"
# Manual extract (specify transcript; project prefers project_id from SessionEnd job)
nokori extract --session ~/.claude/projects/.../session.jsonl
nokori extract --session .../session.jsonl --project myrepo-a1b2c3d4
# Or dry-run preview
nokori extract --session ~/.claude/projects/.../session.jsonl --dry-run
# Consume all pending extract jobs
nokori extract
Pipeline: read transcript (single file ≤ 50MB) → compress (keep user messages, truncate AI replies) → LLM extract candidates → merge with existing rules (SAME/BROADER/CONTRADICTS/UNRELATED).
LLM call shape: extract and merge use system (fixed instructions) + user (untrusted body); transcript / candidates / existing rule text are wrapped in --- BEGIN UNTRUSTED DATA --- blocks to reduce prompt-injection from tool output. Remote endpoints use OpenAI-compatible /v1/chat/completions; when unconfigured, fallback is claude -p (system via --system-prompt, body on stdin).
Merge decisions (implementation) — LLM relation letters A–E map to SAME / BROADER / NARROWER / CONTRADICTS / UNRELATED:
| Decision | Behavior |
|---|---|
SAME (A) + existing candidate |
Add evidence; high correction may activate immediately, else activate per evidence rules |
SAME (A) + existing active / dormant |
No new rule; add_evidence(..., "same_extraction", 1) on existing row, full history kept |
| BROADER / CONTRADICTS (B/D) | Insert new rule and supersede old; if same round already has A, supersede to A’s rule, no second active insert |
| NARROWER (C) | Insert new rule (coexists with existing); if same round also has SAME (A), still insert this candidate |
| UNRELATED (E) | Insert new candidate, independent of neighbors |
| No strong relation | Insert new candidate |
Merge LLM failure (neighbors exist but relation JSON invalid/timeout): current candidate still inserted as standalone rule, but merge_ok=false; nokori extract does not mark transcript extracted; job stays pending (checkpoint keeps processed candidates) for retry.
Extract LLM failure (or non-JSON): no candidates inserted; job stays pending.
Neighbor backfill (intentional in v0.1): when BM25 pre-filter yields fewer than 5 rules, recent rules by updated_at are added for the LLM, costing extra tokens and many UNRELATED hits — reduces missed merges with zero token overlap; no toggle. Tradeoff: prefer extra LLM calls over missing SAME/B/D merges.
Without LLM config, Nokori tries claude -p --model haiku as fallback (prompt on stdin, not argv).
Database
- SQLite
rules.db, created automatically on first use - If the database is incompatible with the current nokori version, operations error; run
nokori exportfirst, or use a freshNOKORI_DATA_DIR/nokori reset
Rule lifecycle
Status names are English; meanings are in Glossary. This section is for fine-tuning.
candidate → active → dormant → may reactivate or archived
↘ merged (superseded by newer rule)
| Status | Injected? | Gated? | How it gets here |
|---|---|---|---|
candidate |
No | No | Auto-extract, moderate confidence, observe first |
active |
Yes | Maybe, if HOT and type matches | Manual high correction, or enough evidence |
dormant |
Yes, but at most WARM when hit | No | 30 days without a “strong” hit (see last_hit) |
merged |
No | No | Superseded by newer rule |
archived |
No | No | You dismiss, or candidate cleaned up |
Activation conditions
- Manual
nokori addor extract merge:high+correctioncandidate → directlyactive(includes initialuser_correctionevidence) - Pure AI evidence (including cross-project
shadow_hot):evidence_score >= 2across>= 2active days
last_hit semantics: used for dormant scan (created_at if last_hit missing). Updated when: (1) formal pool HOT/WARM actually written to context; (2) dormant rule hits retrieval threshold and reactivates this turn. hit_count still increments only on HOT injection.
Dormant reactivation: when retrieval score reaches HOT tier, this turn still injects as WARM (no gate); DB this turn sets status=active and updates last_hit; next turn may HOT + gate (if correction/anti_pattern). Matches UserPromptSubmit hook behavior.
Project ID
Nokori resolves the project root via git rev-parse --show-toplevel and builds <dirname>-<first 8 chars of path hash> as project_id. Same repo name at different paths does not collide. Non-git directories fall back to cwd path hash.
Global Promotion
Each UserPromptSubmit searches formal pool ∪ shadow pool once (BM25 + optional embedding RRF), then splits by pool: only formal pool HOT/WARM inject; shadow pool HOT and WARM both call record_shadow_hit (promotion only, not injected into current chat). After ≥3 distinct project_id hits, rule becomes global (no second confirmation, v0.1 product choice). preference rules do not participate.
Shadow Pool
Summary: while coding in project A, validated rules from project B still score, but are not injected into A’s chat — only used to decide whether the rule should go global.
- Same retrieval as current-project rules (BM25; embedding + RRF when enough rules)
- HOT or WARM both record a shadow hit (promotion evidence)
- At most 1 hit per other project per calendar day (same project same day does not stack)
- ≥3 distinct projects hit → rule becomes
global, no manual confirm
On a new project with zero rules, shadow pool still runs if promotion is on — builds cross-project consensus from scratch. Disable: NOKORI_PROMOTION_ENABLED=0.
Progress: nokori status shows shadow_hits and N/3 projects=....
Async Extract Mode (auto-extract after session close)
export NOKORI_EXTRACT_MODE=async
manual(default): session end writes a pending job; you runnokori extractyourselfasync: session end tries to run extract in the background (if a process is already running, queue only, no duplicate spawn)
Logs: ~/.nokori/logs/async-extract.log. Without LLM config, tries local claude -p fallback.
If {data_dir}/extract.lock is held (another extract instance, or stale lock), SessionEnd does not auto-spawn; pending job remains — run nokori extract manually later.
If the transcript is still appended after SessionEnd (file mtime changes), nokori extract refreshes job mtime and keeps pending, does not silently drop the job.
Corrupt extract-*.json (unparseable) moves to {data_dir}/jobs/bad/ during list_jobs / nokori extract / SessionStart maintenance, avoiding zombie jobs.
Optional: NOKORI_EXTRACT_DEFER_ACTIVE=1 — in async mode, if other sessions without SessionEnd remain (active_sessions/ with empty ended_at, count_open_sessions), current SessionEnd writes job only, no fork of nokori extract; extract after other sessions end or via manual/next SessionEnd trigger.
NOKORI_SESSION_IDLE_SECONDS ([session] idle_seconds) does not participate in defer; only used for “active” display in nokori status (open + recent touch heartbeat).
Extract jobs are consumed by nokori extract (manual or async child). In async mode, SessionStart retries background extract spawn when a pending job exists and extract lock is free. nokori extract uses {data_dir}/extract.lock (Unix / Windows) to prevent concurrent processing; if another instance runs, exit 2 with (extract already running) (distinct from exit 0 for no pending job).
Hot cache
SessionStart finds “previous session transcript”:
- Prefer
{data_dir}/transcript_index/(previous/current pointers written by SessionEnd) — the last session that ended normally in that directory, not necessarily the oldest*.jsonlby mtime. - Fallback: latest
*.jsonlin the same directory with mtime strictly before the current file (heuristic, scan at most 50 files).
If the previous session was not extracted yet, read the last 3 user messages from the file tail for injection (500 chars, separate budget). Dormant pseudo-HOT, shadow counts, HOT hit_count are all written in UserPromptSubmit this turn, not deferred to next SessionStart.
Shadow and candidate activation: cross-project shadow HOT calls add_evidence(..., shadow_hot, 1). If another project’s rule is still candidate, repeated shadow hits on different days may satisfy pure AI activation (score≥2 and 2 active days) — unlike “promotion only” intuition, v0.1 intentionally allows cross-project retrieval evidence to activate.
Maintenance
Maintenance runs automatically on SessionStart (interval checks):
- Dormant scan (every 7 days): active with no hit for 30 days → dormant
- Candidate cleanup (scan interval at most every 30 days): delete ordinary candidates with created_at ≥20 calendar days,
anti_patterncandidates ≥40 days (not “alive 30 days”) - Unmerge check (at most every 90 days):
status=mergedrules whosesuperseded_bytarget was deleted or is dormant/archived revert todormant; immediate orphan unmerge after candidate cleanup deletes anchor rule - Session file cleanup: delete registry files in
active_sessions/ended more than 60 days ago - Hook coalesce cleanup: delete
hook_coalesce/claim files older than 24 hours (avoids buildup when many prompts run) - Prompt ack cleanup: delete
prompt_submit_ack/andcursor_deferred/files older than 24 hours;SessionEndalso removes that session’s ack/deferred directory - Injection cleanup (scan interval at most every 7 days): delete
injectionsrows older than 30 days (dismiss only checks 24h; buffer retained)
Manual trigger:
nokori maintain
Retrieval engine
How are relevant rules found? Keywords first (BM25), semantic vectors when you have enough rules, then RRF merges both lists. HOT/WARM tiers control how much text goes into context.
BM25 (default, zero dependencies)
- Document fields:
trigger_text,trigger_variants,search_terms,action - Latin text: lowercase word tokens (≥ 2 chars)
- CJK text: primarily bigrams; single CJK characters keep unigrams (higher recall)
- Mixed text switches automatically
Embedding (optional)
When rules ≥ 20 (in the batch searched for this prompt) and a remote API is configured or pip install nokori[local-embed] is installed, semantic retrieval is added automatically.
NOKORI_EMBED_ENABLED=1 forces an attempt (small pools may still use BM25 only on first pass; see below).
Two thresholds (easy to confuse):
| Scenario | Count scope | Purpose |
|---|---|---|
SessionStart embed kickstart |
All active+dormant in DB |
Whether to spawn embed server in background (≥20 may spawn, regardless of how few rules the current project has) |
| UserPromptSubmit retrieval | Formal∪shadow pool size for this prompt | Whether this prompt uses embedding RRF |
Partial index: with embed enabled, rules without a rule_embeddings row rely on BM25 only in RRF (just activated, post-import, or failed index). Semantic search uses only rule_embeddings rows matching the current configured embed model name; after model or dimension change, reindex / re-add or import. nokori health embed.index warns on missing rows; remote probe counts ok only on HTTP 2xx (401/404 not healthy).
Remote API mode:
export NOKORI_EMBED_BASE_URL="http://localhost:11434/v1"
export NOKORI_EMBED_MODEL="nomic-embed-text"
# NOKORI_EMBED_DIMENSIONS defaults to unset (model’s own dims); set only for OpenAI text-embedding-3 etc.
Local model mode (no URL config):
pip install nokori[local-embed]
# Or dev install: pip install -e ".[local-embed]"
Installing [local-embed] pulls sentence-transformers>=3.0 (required for Granite encode_query / encode_document; ST 2.x is unsupported). Model weights (ibm-granite/granite-embedding-97m-multilingual-r2, ~97M params, 384-dim) download to ~/.nokori/models/ at these times (not in hooks — avoids timeout). User prompts use encode_query, indexed rules use encode_document (Granite R2 retrieval API). After upgrading from an older default model, run nokori embed prefetch and re-index rules (add / import / edit trigger fields) so rule_embeddings match the new model_version:
| When | Notes |
|---|---|
pip install …[local-embed] |
Auto prefetch after install (pip install -e too) |
nokori install |
Prefetches if [local-embed] installed, regardless of hook registration |
nokori embed prefetch |
Manual download or retry |
With no remote embed endpoint and ≥20 retrievable rules, the embed shared process loads the model from that directory.
Hook behavior (NOKORI_EMBED_SERVER_AUTO_START=1, default on):
- SessionStart: if local weights exist in cache → non-blocking
spawnembed server; missing weights log only, no block, noimport sentence_transformersin hook - UserPromptSubmit: if server not
ping-able → background spawn, this turn BM25 only; RRF usually from next turn - No model download or long load inside hooks (avoids Claude hook timeout)
nokori embed start can warm up early; NOKORI_EMBED_ENABLED=1 forces embed attempt (even if rules <20); first message on tiny pools may still be BM25-only.
Priority: remote API (base_url set) > local embed server ([local-embed] installed) > BM25 only. If server not ready, fall back to BM25; do not load the model in every hook subprocess.
Both scores merge via RRF, then HOT/WARM tiers apply.
Platform note: local embed is macOS / Linux only (embed.sock). Windows: BM25 only or remote NOKORI_EMBED_BASE_URL.
Local embed management (Unix):
nokori embed prefetch # Download local model weights (skip if pip/install already did)
nokori embed start # Background shared server (hooks also auto-start on demand)
nokori embed status # Process / socket / idle config
nokori embed stop # Graceful shutdown (SIGTERM + IPC shutdown)
# nokori embed serve # Foreground debug; exits after NOKORI_EMBED_SERVER_IDLE seconds idle
Local embed server Unix socket lives under NOKORI_DATA_DIR, no IPC auth (acceptable for single-user local use; do not put the data dir on a shared multi-user path).
Injection tiers
| Tier | Condition | Injected content |
|---|---|---|
| HOT | top-1 and clearly above top-2 + minimum evidence; only 1 hit also needs rrf_score > 0.01 and ≥3 matched tokens |
trigger + action + rationale |
| WARM | others in top-5 (with minimum evidence) | trigger + action one line |
| COLD | outside top-5 | not injected |
Minimum evidence: ≥2 query token overlap; or 1 token + trigger variant hit; or embedding cosine ≥ 0.55. Pure embedding hits may have empty matched_tokens (still pass cosine threshold for HOT/WARM).
Injection budget: 1500 chars (rules) + 500 chars (hot cache, separate). Only rules actually written to context are logged in injections and update last_hit / HOT hit_count (truncated-by-budget rules are not).
Full CLI reference
# Rule management
nokori add [--trigger "..." --action "..." --source-type ... --confidence ...]
nokori list [--all] [--project <id>]
nokori show <short_id>
nokori dismiss <short_id>
nokori edit <short_id> [--trigger ...] [--action ...] [--variants ...] [--terms-en ...] [--terms-zh ...]
# Extraction
nokori extract [--session <path>] [--dry-run]
# Debugging
nokori test "<prompt>" [--project <id>]
nokori status # Includes promotion progress: per project rule N/3 distinct projects shadow HOT
nokori logs
nokori health
# Maintenance
nokori maintain
nokori reset [--force] # Non-interactive terminals require --force
# Local embed shared process (Unix; optional)
nokori embed prefetch | start | stop | status
# Import / export (JSON version field = rules.db schema, currently 2)
nokori export <path.json>
nokori import <path.json>
# Installation
nokori install [--claude | --cursor | --all] [--dry-run | --uninstall | --disable | --enable | --no-prefetch-embed]
Environment variables
| Variable | Default | Description |
|---|---|---|
NOKORI_DATA_DIR |
~/.nokori |
Data root directory |
NOKORI_MAX_INJECTION_CHARS |
1500 |
Injection character limit |
NOKORI_GATE_ENABLED |
1 |
Enable Gate |
NOKORI_GATE_TTL_SECONDS |
600 |
Marker expiry; 0 = never expire |
NOKORI_GATE_MATCHER |
Edit|Write|MultiEdit|Bash|NotebookEdit |
Layer 2: regex for tool_name blocked inside hook (use .* for any tool); see Gate two-layer matching |
NOKORI_EXTRACT_MODE |
manual |
manual / async |
NOKORI_EXTRACT_DEFER_ACTIVE |
0 |
1 = defer async extract fork when other sessions active |
NOKORI_SESSION_IDLE_SECONDS |
1800 |
No heartbeat in active_sessions beyond this → inactive |
NOKORI_HOT_CACHE |
1 |
SessionStart hot cache |
NOKORI_PROMOTION_ENABLED |
1 |
Shadow pool and cross-project promotion; 0 disables scenario C |
NOKORI_HOOK_EMBED_TIMEOUT |
2 |
Hook remote embed timeout (seconds) |
NOKORI_EMBED_SERVER_IDLE |
3600 |
Local embed process idle exit (seconds) |
NOKORI_EMBED_SERVER_AUTO_START |
1 |
Hooks auto-start embed server on demand |
NOKORI_LLM_BASE_URL |
— | OpenAI-compatible chat completions endpoint |
NOKORI_LLM_MODEL |
— | LLM model name |
NOKORI_LLM_API_KEY |
— | LLM API key |
NOKORI_EMBED_ENABLED |
0 (auto when active+dormant≥20) |
Force embedding on |
NOKORI_EMBED_BASE_URL |
— | OpenAI-compatible embeddings endpoint |
NOKORI_EMBED_MODEL |
— | Embedding model name |
NOKORI_EMBED_API_KEY |
— | Embedding API key |
NOKORI_EMBED_DIMENSIONS |
0 (omit, use model default) |
Vector dimensions (only for models that support the parameter) |
NOKORI_EMBED_CHUNK_SIZE |
4000 |
Text chunk size in characters |
NOKORI_EMBED_CHUNK_COUNT |
2 |
Max chunks per rule |
NOKORI_STRICT |
0 |
1 = hook errors propagate (debug; default fail-open) |
NOKORI_DISABLED |
0 |
Disable entirely |
NOKORI_HOOK_COALESCE |
1 |
When Claude + Cursor both register hooks: only first invocation per event runs logic (0 = off, may double-inject) |
NOKORI_DISMISS_PHRASE |
dismiss |
Chat verb to retire rules (verb + short_id); see Dismiss |
NOKORI_LOG_LEVEL |
warn |
Log level (debug also enables [diag] hook traces) |
NOKORI_HOOK_DEBUG |
0 |
1 = verbose per-hook [diag] lines in hook.log |
Environment variables only (no config.toml field; see config.toml.example):
| Variable | Default | Description |
|---|---|---|
NOKORI_CLAUDE_HOME |
~/.claude |
Directory for settings.json read/written by nokori install |
NOKORI_TRANSCRIPT_EXTRA_ROOTS |
— | Extra allowed transcript roots, os.pathsep-separated (path safety checks) |
NOKORI_EXTRACTING |
— | Internal: prevents recursion in claude -p fallback child; do not set in user shell or async extract |
All LLM/embedding endpoints are compatible with Ollama, LMStudio, vLLM, OpenRouter, OpenAI, and any /v1/chat/completions + /v1/embeddings server.
Configuration file
Besides environment variables, Nokori supports TOML at ~/.nokori/config.toml (path follows NOKORI_DATA_DIR).
The repo root has a full template config.toml.example (all options, defaults, allowed values, and notes).
Priority: environment variables > config.toml > built-in defaults.
# ~/.nokori/config.toml
log_level = "info"
dismiss_phrase = "dismiss"
[llm]
base_url = "http://127.0.0.1:8317/v1"
model = "deepseek-v4-flash"
api_key = "sk-xxx"
[embed]
# Remote OpenAI-compatible API (same [embed] table as server params below — do not duplicate [embed] headers)
base_url = "https://api.example.com/v1"
model = "text-embedding-v4"
api_key = "sk-xxx"
# dimensions = 0 # unset or 0 = do not pass to API (use model default dimensions)
chunk_size = 4000
chunk_count = 2
enabled = true
# Local embed shared process (when base_url unset and pip install nokori[local-embed])
# hook_timeout_seconds = 2
# server_idle_seconds = 3600
# server_auto_start = true
[gate]
enabled = true
ttl_seconds = 600
matcher = "Edit|Write|MultiEdit|Bash|NotebookEdit"
[extract]
mode = "manual"
# defer_when_active = false # Defer async extract when other open sessions exist
[hot_cache]
enabled = true
[promotion]
enabled = true
[session]
# idle_seconds = 1800
Every field maps to an environment variable (see quick reference in config.toml.example). Missing file is ignored silently; env-only mode works fine.
Note: [gate] matcher only affects whether Nokori blocks inside the hook; whether PreToolUse invokes the hook is controlled by ~/.claude/settings.json, see Gate two-layer matching above. Full dismiss_phrase details in Dismiss.
Data storage
All data is stored locally under ~/.nokori/:
~/.nokori/
├── config.toml # Config file (optional; env vars take precedence)
├── rules.db # SQLite (WAL mode): rules + indexes + metadata
├── jobs/ # Extract job queue
├── active_sessions/ # Session registry
├── gate_markers/ # Gate markers (by session + prompt_hash)
├── hook_coalesce/ # Dedup claims when Claude + Cursor both fire hooks
├── logs/
│ ├── hook.log # Hook process logs
│ ├── pipeline.log # Extract/merge logs
│ ├── async-extract.log # async mode child stderr
│ └── embed-server.log # Local embed server (if enabled)
├── models/ # Local embed weights (pip [local-embed] / install / embed prefetch)
├── embed.sock # Local embed IPC (Unix)
└── extract.lock # Extract single-instance lock
- Zero network sync, purely local
- Rules contain no source code, only behavioral descriptions
- LLM calls send compressed transcript snippets (not source code)
- Point at local Ollama for fully offline operation
- Database: tied to the current nokori version; after upgrade or on a new machine, if the DB will not open,
nokori exportfirst, or use a freshNOKORI_DATA_DIR/nokori reset.
Relationship with existing systems
| System | Relationship |
|---|---|
| CLAUDE.md | Complementary. Nokori does not edit CLAUDE.md; rules are dynamic “when X, do Y” |
| Claude Code auto-memory | No conflict. Memory skews factual; Nokori skews behavioral rules |
| Other memory plugins | Hooks can coexist; avoid stacking too many “stuff context” plugins |
Development
git clone https://github.com/KorenKrita/nokori.git
cd nokori
python3.11+ -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest tests/ # Do not use system python -m pytest (may collect 0 tests)
Project constraints:
- Zero runtime dependencies (
dependencies = []) - Pure Python stdlib + urllib for API calls
- No LLM calls on interactive hot paths (UserPromptSubmit / PreToolUse)
- All hooks wrapped in top-level try/except; failures return pass-through
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nokori-0.1.0.tar.gz.
File metadata
- Download URL: nokori-0.1.0.tar.gz
- Upload date:
- Size: 165.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6eef8d6730a687846c1efedce1c4b569012c8af8b7b2f2b0a93f05887925f0b7
|
|
| MD5 |
47d686e737283d1d979d62deeae0ff4b
|
|
| BLAKE2b-256 |
ebf50489c832481f9ca92e3f880927b4a5eb8ed363fe8b8edf4c24792f49ddbe
|
File details
Details for the file nokori-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nokori-0.1.0-py3-none-any.whl
- Upload date:
- Size: 123.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e37eafb998dd892a7512990d1d5ebbca87a42a6a87e344311f7254d281d0ad68
|
|
| MD5 |
5e3ced0fdaa6d6bcfeafa9313c520a0d
|
|
| BLAKE2b-256 |
e36c2fd59ed32021c4aa6fd3dd8cca450d9adeea5401ff8dbe499e05ae6e0428
|