Skip to content
Conduit

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.

secondfry/claude-code-self-learncommunitylistedunreviewed

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

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 23, 2026

self-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 --user timer (cron fallback) on Linux, and a Scheduled Task on Windows. Requirements: Node on PATH (for the hooks) and Python 3 (python3/py) for the pipeline.

Model note: the shipped default learn_model is claude-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.json to run on a different model (see Configuration); if you point learn_gateway at 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_MIN minutes ⇒ 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-learnclaude-reflect
Learnsfacts + skillscorrections (facts) + skill discovery
Fact storenative Auto-memory (memory/*.md + MEMORY.md)CLAUDE.md hierarchy
Triggerout-of-band scheduler, idle-gatedauto capture hook + manual /reflect
In-session costnone — runs headless, out of processa review turn when you run /reflect
Human in the loopno (auto-managed, rollback-guarded)yes — you approve each learning
Skill-edit safetysnapshot → validate → rollback—
Lifecycle curatoryes—
PlatformsmacOS, Linux, WindowsmacOS, 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.md index). It routes by scope: cross-project user/feedback facts go to the global store ~/.claude/memory/; project/reference facts 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' scratch agent-cwd/, and the lock/flag files. The Python resolves these via ~/.claude/self-learn regardless of where the plugin is installed, so updating or moving the plugin never disturbs accumulated state.

Layout

PathRole
.claude-plugin/plugin.jsonPlugin manifest
.claude-plugin/marketplace.jsonLocal marketplace entry (single-plugin)
agents/{fact-extractor,skill-distiller}.mdThe two learner subagents
commands/*.md/distill-skill, /extract-facts, /curator-status, /prune-skills, /archive-skill, /restore-skill, /pin-skill
hooks/hooks.jsonWires the 4 hooks (SessionStart, PreToolUse, PostToolUse, Stop) in exec form (command: node) — no shell dependency
hooks/run.mjsCross-platform launcher: pipes hook stdin to a Python pipeline script, fail-safe
hooks/session-init.mjsSessionStart: self-register the OS scheduler + run the throttled curator
hooks/lib/py.mjsPer-OS Python 3 interpreter resolution (python3/py/python)
pipeline/dispatch.pyThe out-of-band loop. Scan → idle/watermark gate → invoke learners headlessly → poke curator. --seed marks the existing backlog as already-considered
pipeline/curator.pySkill lifecycle state machine + CLI
pipeline/analyze_turn.pyStop-hook: skill use/view telemetry only (no nudges)
pipeline/backup_skill.pyPreToolUse: snapshot SKILL.md before edit
pipeline/validate_skill.pyPostToolUse: SKILL.md validate → rollback → stamp provenance → record patch; memory/*.md advisory format check
pipeline/lib/transcript.pyJSONL transcript parser + is_real_user_msg noise filter
pipeline/lib/store.pyAtomic state store (flock on POSIX, msvcrt on Windows): per-skill telemetry + per-transcript watermarks (writes ~/.claude/self-learn/state.json)
scheduler/install.mjsCross-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:

OSBackendRegistered at
macOSlaunchd LaunchAgent~/Library/LaunchAgents/ru.secondfry.self-learn.plist
Linuxsystemd --user timer (cron fallback)~/.config/systemd/user/self-learn.timer
WindowsScheduled Taskself-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.

keyenv vardefaultmeaning
dispatch_enabledSL_DISPATCHtruefalse disables the whole out-of-band loop
facts_enabledSL_FACT_NUDGEtruefalse disables the fact-extraction branch
skills_enabledSL_SKILL_NUDGEtruefalse disables the skill-distillation branch
idle_minSL_IDLE_MIN30Debounce: minutes a transcript must be idle before it's "finished"
batch_msgsSL_BATCH_MSGS1Min NEW user messages since watermark → fact pass (1 = any)
distill_thresholdSL_DISTILL_THRESHOLD1Min NEW tool calls since watermark → skill pass (1 = any)
min_file_editsSL_MIN_FILE_EDITS1Min file edits for a skill pass (1 = any real work)
max_per_passSL_MAX_PER_PASS8Max learner invocations per dispatcher pass (backlog drain rate)
agent_timeoutSL_AGENT_TIMEOUT600Per-subagent wall-clock ceiling (seconds)
learn_modelSL_LEARN_MODELclaude-sonnet-4-6Model id passed via --model (format is gateway-coupled — see below)
learn_gatewaySL_LEARN_GATEWAY""ANTHROPIC_BASE_URL for the headless runs only. Empty = use the gateway from settings.json.
learn_token_envSL_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_binSL_CLAUDE_BIN(auto)Override the resolved claude binary path
stale_after_daysSL_STALE_AFTER_DAYS30Curator: idle days → mark a skill stale
archive_after_daysSL_ARCHIVE_AFTER_DAYS90Curator: idle days → archive (×2 if use_count≥3)
curate_interval_daysSL_CURATE_INTERVAL_DAYS7Curator: min days between auto-curate passes
curate_min_skillsSL_CURATE_MIN_SKILLS5Curator: 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_PASS caps 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=0 in ~/.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-learn to 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
    
↑↓ navigate openesc close