skill
claude-code-self-learn
Hermes-style out-of-band self-learning loop for Claude Code: distills finished transcripts into native Auto-memory facts and reusable skills.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/secondfry/claude-code-self-learn itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 23, 2026self-learn — a Hermes-style self-learning loop, packaged as a Claude Code plugin
A native port of Hermes Agent's self-improvement loop, built from the design lessons of two prior-art Claude Code plugins (BayramAnnakov/claude-reflect, UniM0cha/claude-self-improving-skills).
This repo packages that loop as a plugin: the subagents, slash commands, and edit-safety hooks install through Claude Code's plugin system; the one piece a plugin cannot provide — an out-of-band background scheduler — self-registers on first session against whatever scheduler the host OS provides.
Quick install
Run entirely from a shell (no Claude session needed — the claude CLI does the
plugin steps):
claude plugin marketplace add secondfry/claude-code-self-learn
claude plugin install self-learn@secondfry-self-learn
Restart any running Claude Code session to pick up the plugin's hooks/agents/commands. The SessionStart hook then self-registers the background dispatcher against the host OS scheduler automatically — no extra step on any platform. See Install below for what that registration does and local-dev variants.
Platform support: macOS, Linux, and Windows. The hooks run through Node (present wherever Claude Code runs from an npm/fnm install) and the dispatcher is pure Python 3. The scheduler self-registers per OS: a launchd LaunchAgent on macOS, a
systemd --usertimer (cron fallback) on Linux, and a Scheduled Task on Windows. Requirements: Node onPATH(for the hooks) and Python 3 (python3/py) for the pipeline.
Model note: the shipped default
learn_modelisclaude-sonnet-4-6(Claude Sonnet 4.6), a public Anthropic API model id — the learners run against it out of the box. Override it in~/.claude/self-learn/config.jsonto run on a different model (see Configuration); if you pointlearn_gatewayat a proxy, keep the id format coupled to that gateway.
How it differs from Hermes / Odysseus (and why)
Both reference systems own their agent loop, so they learn inline and live —
Hermes mirrors every turn into memory on a background thread; Odysseus writes via
a manage_memory tool call mid-turn. A Claude Code plugin cannot hook the
turn loop, so that path is closed to us. Instead our substrate is the transcript
JSONL that Claude Code already writes to disk, and learning runs out-of-band.
The trigger design copies the one out-of-band mechanism both references DO have:
- Idle-gate (Hermes' curator, ≥2h idle): only process a transcript whose file
has been untouched for
SL_IDLE_MINminutes ⇒ the session is finished. No partial reads; native Auto-memory (which writes live) finished long ago, so we never race it. - Watermark counter (Odysseus' event-threshold): a per-transcript record of the msg/tool-call totals at last processing. Reprocess only when enough NEW work has landed. The watermark IS the bookkeeping.
Compared to claude-reflect
claude-reflect is the closest widely-used prior art. Both learn from the history Claude Code already writes; the core bets differ.
| self-learn | claude-reflect | |
|---|---|---|
| Learns | facts + skills | corrections (facts) + skill discovery |
| Fact store | native Auto-memory (memory/*.md + MEMORY.md) | CLAUDE.md hierarchy |
| Trigger | out-of-band scheduler, idle-gated | auto capture hook + manual /reflect |
| In-session cost | none — runs headless, out of process | a review turn when you run /reflect |
| Human in the loop | no (auto-managed, rollback-guarded) | yes — you approve each learning |
| Skill-edit safety | snapshot → validate → rollback | — |
| Lifecycle curator | yes | — |
| Platforms | macOS, Linux, Windows | macOS, Linux, Windows |
The distinguishing bet is fully out-of-band. claude-reflect captures
corrections live and applies them when you run /reflect, keeping a human review
step. self-learn instead processes finished transcripts on a scheduler, so
learning never occupies or bills an interactive turn — and it writes Claude
Code's native Auto-memory format for facts while also distilling skills. The
trade is the review gate for zero in-session cost and automatic skill distillation.
The wider field (e.g. claude-mem, agentmemory) mostly builds memory as an MCP server with RAG-style retrieval injected at session start; self-learn deliberately runs no MCP server and adds nothing to the live context — it writes the native on-disk format Claude Code already reads.
Division of labor
- Facts (declarative memory) → the fact-extractor subagent writes Claude
Code's native Auto-memory format (one file per fact +
MEMORY.mdindex). It routes by scope: cross-projectuser/feedbackfacts go to the global store~/.claude/memory/;project/referencefacts to the per-project~/.claude/projects/<proj>/memory/. - Skills (procedures) → the skill-distiller subagent: turn a session's
proven technique into a reusable
SKILL.md, then curate those skills over time.
The loop
┌─ OS scheduler every ~1800s (launchd / systemd / Scheduled Task)┐
│ self-learn-dispatcher → pipeline/dispatch.py │
└───────────────────────────────┬───────────────────────────────┘
▼
┌─ dispatch.py (OUT OF SESSION) ────────────────────────────────┐
│ scan ~/.claude/projects/**/*.jsonl │
│ idle-gate: file mtime older than SL_IDLE_MIN → finished │
│ watermark: new user-msgs / tool-calls since last pass │
│ FACT branch: new real-user-msgs >= SL_BATCH_MSGS │
│ SKILL branch: new tool-calls >= SL_DISTILL_THRESHOLD │
│ AND file-edits >= SL_MIN_FILE_EDITS │
│ cap SL_MAX_PER_PASS learner invocations per pass (drain) │
│ then poke curator.py --auto (internally interval-gated) │
└───────────────┬──────────────────────────┬────────────────────┘
claude -p --agent fact-extractor claude -p --agent skill-distiller
--permission-mode auto (classifier-gated) --add-dir ~/.claude/{projects,skills,memory}
│ │
▼ ▼
┌─ fact-extractor subagent ────┐ ┌─ skill-distiller subagent ─────────┐
│ dedup vs MEMORY.md → update │ │ patch existing → umbrella → add │
│ or create <type>_<slug>.md │ │ ref file → create new (last resort)│
│ (native format). Routes by │ │ Class-level names only. Skips │
│ scope. Never secrets. │ │ one-offs. │
└──────────────┬───────────────┘ └───────────────┬────────────────────┘
writes memory/*.md (global or proj) writes ~/.claude/skills/<name>/SKILL.md
│ │
▼ ▼
┌─ PreToolUse backup + PostToolUse validate (Write|Edit|MultiEdit) ┐
│ backup_skill.py: snapshot SKILL.md before edit. │
│ validate_skill.py: SKILL.md → validate → ROLL BACK if broken │
│ → stamp provenance → record patch. memory/*.md → ADVISORY. │
└──────────────────────────────────────────────────────────────┘
┌─ Stop hook (run.mjs → analyze_turn.py) — telemetry only ─────────┐
│ records skill use/view per stop for the curator's idle clock │
└──────────────────────────────────────────────────────────────────┘
Code vs. state (the plugin split)
- Code lives in this plugin (
pipeline/,hooks/,agents/,commands/,scheduler/). It is stateless and relocatable — every internal reference is resolved relative to the file (__file__) or${CLAUDE_PLUGIN_ROOT}. - Mutable state lives in
~/.claude/self-learn/and is NEVER written into the plugin repo:state.json(skill telemetry + per-transcript watermarks), the dispatcher log,skill_backups/, the learners' scratchagent-cwd/, and the lock/flag files. The Python resolves these via~/.claude/self-learnregardless of where the plugin is installed, so updating or moving the plugin never disturbs accumulated state.
Layout
| Path | Role |
|---|---|
.claude-plugin/plugin.json | Plugin manifest |
.claude-plugin/marketplace.json | Local marketplace entry (single-plugin) |
agents/{fact-extractor,skill-distiller}.md | The two learner subagents |
commands/*.md | /distill-skill, /extract-facts, /curator-status, /prune-skills, /archive-skill, /restore-skill, /pin-skill |
hooks/hooks.json | Wires the 4 hooks (SessionStart, PreToolUse, PostToolUse, Stop) in exec form (command: node) — no shell dependency |
hooks/run.mjs | Cross-platform launcher: pipes hook stdin to a Python pipeline script, fail-safe |
hooks/session-init.mjs | SessionStart: self-register the OS scheduler + run the throttled curator |
hooks/lib/py.mjs | Per-OS Python 3 interpreter resolution (python3/py/python) |
pipeline/dispatch.py | The out-of-band loop. Scan → idle/watermark gate → invoke learners headlessly → poke curator. --seed marks the existing backlog as already-considered |
pipeline/curator.py | Skill lifecycle state machine + CLI |
pipeline/analyze_turn.py | Stop-hook: skill use/view telemetry only (no nudges) |
pipeline/backup_skill.py | PreToolUse: snapshot SKILL.md before edit |
pipeline/validate_skill.py | PostToolUse: SKILL.md validate → rollback → stamp provenance → record patch; memory/*.md advisory format check |
pipeline/lib/transcript.py | JSONL transcript parser + is_real_user_msg noise filter |
pipeline/lib/store.py | Atomic state store (flock on POSIX, msvcrt on Windows): per-skill telemetry + per-transcript watermarks (writes ~/.claude/self-learn/state.json) |
scheduler/install.mjs | Cross-platform scheduler installer: renders + registers a launchd LaunchAgent (macOS), systemd --user timer/cron (Linux), or Scheduled Task (Windows). Idempotent + self-healing |
Install
Quick install is the whole happy path. This section covers the variants: local development and how the scheduler self-registers.
Local development (run straight from a checkout, no marketplace):
claude --plugin-dir /path/to/claude-code-self-learn
Scheduler. Nothing to wire by hand on any platform — the SessionStart hook
(hooks/session-init.mjs) calls scheduler/install.mjs, which registers the
dispatcher for the host OS and refreshes it (idempotently) each session:
| OS | Backend | Registered at |
|---|---|---|
| macOS | launchd LaunchAgent | ~/Library/LaunchAgents/ru.secondfry.self-learn.plist |
| Linux | systemd --user timer (cron fallback) | ~/.config/systemd/user/self-learn.timer |
| Windows | Scheduled Task | self-learn-dispatcher (Task Scheduler) |
Each backend runs pipeline/dispatch.py ~every 30 min from the plugin's cache
copy (so it survives the source tree moving), using the absolute Python resolved
at install. To force a registration outside a session: node /path/to/claude-code-self-learn/scheduler/install.mjs.
The existing transcript backlog is learned from, not abandoned: the loop
drains it oldest-first (chronological, so a later session's fact corrects an
earlier one — never the reverse), SL_MAX_PER_PASS transcripts per pass. Trigger
a pass immediately — macOS: launchctl kickstart -k gui/$(id -u)/ru.secondfry.self-learn;
Linux: systemctl --user start self-learn.service; Windows: schtasks /Run /TN self-learn-dispatcher.
To skip the backlog and only learn from new work, run this ONCE before installing:
python3 /path/to/claude-code-self-learn/pipeline/dispatch.py --seed.
Configuration
Every tunable lives in ~/.claude/self-learn/config.json — one
human-editable file, read fresh each pass, so changing the model or thresholds
takes effect on the next 30-min run with no scheduler reload. The file is
optional; every key has a built-in default. Each key can also be set by an
SL_* env var, and precedence is env var > config.json > default — env vars
stay useful for a one-off shell override or a scheduler-baked tweak. The
self-learn-config skill (shipped with the plugin) guides edits to this file.
| key | env var | default | meaning |
|---|---|---|---|
dispatch_enabled | SL_DISPATCH | true | false disables the whole out-of-band loop |
facts_enabled | SL_FACT_NUDGE | true | false disables the fact-extraction branch |
skills_enabled | SL_SKILL_NUDGE | true | false disables the skill-distillation branch |
idle_min | SL_IDLE_MIN | 30 | Debounce: minutes a transcript must be idle before it's "finished" |
batch_msgs | SL_BATCH_MSGS | 1 | Min NEW user messages since watermark → fact pass (1 = any) |
distill_threshold | SL_DISTILL_THRESHOLD | 1 | Min NEW tool calls since watermark → skill pass (1 = any) |
min_file_edits | SL_MIN_FILE_EDITS | 1 | Min file edits for a skill pass (1 = any real work) |
max_per_pass | SL_MAX_PER_PASS | 8 | Max learner invocations per dispatcher pass (backlog drain rate) |
agent_timeout | SL_AGENT_TIMEOUT | 600 | Per-subagent wall-clock ceiling (seconds) |
learn_model | SL_LEARN_MODEL | claude-sonnet-4-6 | Model id passed via --model (format is gateway-coupled — see below) |
learn_gateway | SL_LEARN_GATEWAY | "" | ANTHROPIC_BASE_URL for the headless runs only. Empty = use the gateway from settings.json. |
learn_token_env | SL_LEARN_TOKEN_ENV | "" | Name of the env var holding the upstream gateway's auth token (the token itself is never stored in config.json). |
claude_bin | SL_CLAUDE_BIN | (auto) | Override the resolved claude binary path |
stale_after_days | SL_STALE_AFTER_DAYS | 30 | Curator: idle days → mark a skill stale |
archive_after_days | SL_ARCHIVE_AFTER_DAYS | 90 | Curator: idle days → archive (×2 if use_count≥3) |
curate_interval_days | SL_CURATE_INTERVAL_DAYS | 7 | Curator: min days between auto-curate passes |
curate_min_skills | SL_CURATE_MIN_SKILLS | 5 | Curator: min agent-distilled skills before auto-curate acts |
Model ↔ gateway coupling
The model id format depends on which upstream gateway the headless runs hit — they must be set as a pair:
learn_gateway: "https://your-gateway.example.com/raw/anthropic"→learn_model: "claude-sonnet-4-6"(bare id)learn_gateway: "https://your-gateway.example.com/raw/openrouter"→learn_model: "anthropic/claude-sonnet-4-6"(vendor-prefixed)
Leaving learn_gateway empty routes learner traffic through whatever gateway
claude loads from ~/.claude/settings.json. If that is a local/proxy gateway
that rewrites aliases to a fixed backing model, a specific learn_model may be
silently rewritten. To hit a specific real upstream model, set learn_gateway
to bypass it for learner traffic only — interactive sessions are untouched. The
auth token is read at run time from the
env var named by learn_token_env; it is never written into config.json.
Permissions — explicit auto-mode
Each headless learner runs with --permission-mode auto: the auto-mode
classifier approves/denies every action non-interactively. This is REQUIRED —
~/.claude/** is a "sensitive directory" that Claude Code hard-blocks tool-based
writes to under acceptEdits, even with --add-dir or an explicit Write(...)
allow-rule. Auto-mode is the classifier-governed path that permits a legitimate
memory/skill write into the user's own dirs while still gating unsafe actions.
--permission-mode bypassPermissions is deliberately NOT used. Nothing is added
to the interactive session's permissions.allow; normal sessions are unaffected.
Design invariants (why it won't misbehave)
- Never wedges a session. The Stop hook only records telemetry and always approves. Learning happens out-of-process; a crash there can't touch your work.
- Never races native Auto-memory. The idle-gate guarantees we only read finished transcripts, hours after native memory wrote live.
- Never re-processes. The watermark advances once a batch is considered.
- Never abandons the backlog. Existing transcripts are drained oldest-first and learned from; nothing is skipped by default.
- Never bursts.
SL_MAX_PER_PASScaps invocations per pass. - Never deletes. Archiving moves skill dirs to
~/.claude/skills/.archive/. - Only auto-manages what it created (
origin: distilled/provenance: self-learn). - Never stores secrets. The fact-extractor drops tokens, cookies, passwords, and API keys even if pasted in the open.
Removal / disable
- Disable the loop only: set
SL_DISPATCH=0in~/.claude/self-learn/config.json("dispatch_enabled": false) — the dispatcher then no-ops without touching the schedule. Or unregister the scheduled job for your OS (see below). - Disable one branch:
SL_FACT_NUDGE=0/SL_SKILL_NUDGE=0. - Full removal — uninstall the plugin via
/plugin, unregister the scheduler for your OS, and (optionally)rm -rf ~/.claude/self-learnto discard accumulated state/telemetry:# macOS launchctl bootout gui/$(id -u)/ru.secondfry.self-learn 2>/dev/null || true rm -f ~/Library/LaunchAgents/ru.secondfry.self-learn.plist # Linux systemctl --user disable --now self-learn.timer 2>/dev/null || true rm -f ~/.config/systemd/user/self-learn.{timer,service} # Windows (PowerShell) schtasks /Delete /TN self-learn-dispatcher /F