Skip to content
Productivity
Hook

Hooks

What engram runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
engram
1.5k3 skills3 agents1 hook

Claude Code

> /plugin marketplace add nagisanzenin/engram
> /plugin install engram@engram

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesstartup|resume|clear|compact"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
Read hooks/hooks.json

Where it lives

  • hooks/session-start-dsh.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Engram re-anchor hook for DeepSeek Harness's Claude Code hook bridge.
    # dsh's bridge consumes ONLY the JSON hookSpecificOutput.additionalContext
    # shape — plain SessionStart stdout is discarded (documented dsh limitation),
    # so this wrapper emits what Claude Code's JSON hook contract allows and dsh
    # actually reads. Degrades to silence on any failure; prints nothing (valid:
    # no output) when no reviews are due.
    set -u
    command -v python3 >/dev/null 2>&1 || exit 0
    ROOT="${CLAUDE_PLUGIN_ROOT:-}"
    if [ -z "$ROOT" ] || [ ! -f "$ROOT/scripts/engram.py" ]; then
      ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)"
    fi
    [ -f "$ROOT/scripts/engram.py" ] || exit 0
    NUDGE="$(python3 "$ROOT/scripts/engram.py" session-start 2>/dev/null || true)"
    [ -n "$NUDGE" ] || exit 0
    python3 - "$NUDGE" <<'PY' 2>/dev/null || true
    import json, sys
    print(json.dumps({"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": sys.argv[1]}}))
    PY
    exit 0
    
  • hooks/session-start-hermes.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Engram re-anchor hook for Hermes Agent.
    # Two modes, auto-detected:
    #   hook mode (stdin carries Hermes' pre_llm_call JSON payload): emit
    #     {"context": "<nudge>"} once per session, {} on every other call.
    #     Register in ~/.hermes/config.yaml:
    #       hooks:
    #         pre_llm_call:
    #           - command: "/path/to/engram/hooks/session-start-hermes.sh"
    #             timeout: 15
    #   plain mode (stdin empty — e.g. `hermes cron create --no-agent --script …`):
    #     print the nudge as plain text (nothing when nothing is due).
    # On Hermes, /learn is the built-in skill-authoring command, so the nudge's
    # "/learn" is rewritten to "/skill learn" in both modes.
    # Contract (Constitution art. 8): ambient, never nagging — at most one nudge
    # per session, and on ANY failure degrade to silence, never to repetition.
    set -u
    command -v python3 >/dev/null 2>&1 || { printf '{}\n'; exit 0; }
    
    payload="$(cat - 2>/dev/null || true)"
    
    emit_nudge() {  # prints the rewritten nudge (empty when nothing is due)
      python3 "$ROOT/scripts/engram.py" session-start 2>/dev/null | sed 's|/learn|/skill learn|g' || true
    }
    
    # Engine resolution: env override, else self-resolve from this script's location.
    ROOT="${ENGRAM_ROOT:-}"
    if [ -z "$ROOT" ] || [ ! -f "$ROOT/scripts/engram.py" ]; then
      ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)"
    fi
    [ -f "$ROOT/scripts/engram.py" ] || { [ -n "$payload" ] && printf '{}\n'; exit 0; }
    
    # Plain mode: no stdin payload (cron --no-agent, manual run). No JSON, no dedupe
    # (each scheduled run SHOULD deliver); stdout goes to the delivery target verbatim.
    if [ -z "$payload" ]; then
      emit_nudge
      exit 0
    fi
    
    # Hook mode. Dedupe key: the sanitized session id; if extraction fails, fall back
    # to the parent (Hermes) PID so the guard fails CLOSED — at most one nudge per
    # Hermes process — never open (one per LLM call).
    session_id="$(printf '%s' "$payload" | python3 -c 'import sys,json
    try: print(json.load(sys.stdin).get("session_id") or "")
    except Exception: print("")' 2>/dev/null | tr -c 'A-Za-z0-9_-' '_' | cut -c1-80)" || session_id=""
    [ -n "$session_id" ] || session_id="pid-${PPID:-0}"
    
    marker="${TMPDIR:-/tmp}/engram-nudge-${session_id}"
    if [ -e "$marker" ]; then printf '{}\n'; exit 0; fi
    # Unwritable marker dir → we could not remember having nudged, so stay silent:
    # silence over repetition, per the contract.
    if ! { : > "$marker"; } 2>/dev/null; then printf '{}\n'; exit 0; fi
    
    out="$(emit_nudge)"
    if [ -n "$out" ]; then
      printf '%s' "$out" | python3 -c 'import sys,json; print(json.dumps({"context": sys.stdin.read().strip()}))' 2>/dev/null || printf '{}\n'
    else
      printf '{}\n'
    fi
    exit 0
    
  • hooks/session-start.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Engram re-anchor hook: surfaces due reviews at session start.
    # ONE registration (hooks/hooks.json), consumed differently per platform:
    #   - Claude Code / Codex / OpenClaw's codex bundle inject PLAIN stdout into
    #     context, so default output stays plain text.
    #   - ZCode discards plain SessionStart stdout — its runner parses ONLY JSON
    #     (hookSpecificOutput.additionalContext). Its plugin context also exports
    #     ZCODE_PLUGIN_ROOT alongside the legacy CLAUDE_PLUGIN_ROOT, so that var is
    #     both the runtime tell AND a working root: when present, emit the JSON
    #     shape ZCode actually reads. Root resolution never assumes which of the
    #     platform roots identified the runtime correctly, because they all point
    #     at the same install path.
    #   - The manual/config-file route has no plugin-root variable at all; users
    #     who wire this script by hand set ENGRAM_HOOK_FORMAT=json themselves
    #     (INSTALL-ZCODE.md documents exactly that command line).
    # Prints at most two lines (or nothing) — ambient, never nagging (art. 8).
    # Must never break a session: degrade to silence on any failure.
    # Portable across Claude Code and Codex: uses the plugin-root env var if set,
    # else self-resolves relative to this script's own location.
    set -u
    command -v python3 >/dev/null 2>&1 || exit 0
    emit_json() {
      python3 - "$1" <<'PY' 2>/dev/null || true
    import json, sys
    print(json.dumps({"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": sys.argv[1]}}))
    PY
    }
    ROOT="${ZCODE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-}}}"
    if [ -z "$ROOT" ] || [ ! -f "$ROOT/scripts/engram.py" ]; then
      ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)"
    fi
    [ -f "$ROOT/scripts/engram.py" ] || exit 0
    NUDGE="$(python3 "$ROOT/scripts/engram.py" session-start 2>/dev/null || true)"
    [ -n "$NUDGE" ] || exit 0                                   # silent when nothing is due —
    case "${ENGRAM_HOOK_FORMAT:-}" in                           # valid output on every consumer
      json) emit_json "$NUDGE"; exit 0 ;;
    esac
    if [ -n "${ZCODE_PLUGIN_ROOT:-}" ]; then                    # ZCode eats plain text; give it
      emit_json "$NUDGE"                                        # the one shape it will parse
      exit 0
    fi
    printf '%s\n' "$NUDGE"
    exit 0
    
  • hooks/session-start.tsGitHub
    Read the script
    /**
     * Engram — Session Start Hooks
     * =============================
     *
     * Two hooks injected into OpenCode's session lifecycle:
     *
     * system.transform   — fires once per session (firstTransform guard).
     *   Runs `engram.py session-start` for the review-due nudge, then calls
     *   readUpdateSummary() to check for pending plugin updates. Injects both
     *   messages into the system prompt.
     *
     *   readUpdateSummary() reads .engram-update.jsonc at the target directory
     *   (project-level .opencode/ or global ~/.config/opencode/) and returns:
     *     null              — no manifest → silent
     *     "pending"          → "Updates Engram Available!\nRun /engram-update"
     *     "in_progress"      → "Update partially applied. Run /engram-update to continue"
     *     corrupt/absent     → null (gracefully degrades)
     *
     * event(session.idle) — fires a TUI toast when an update manifest exists on disk.
     *   Guard: toastShown (one per session). Calls readUpdateSummary() — if non-null,
     *   shows client.tui.showToast() with "Updates Engram Available!".
     *   Best-effort — all errors caught via .catch(() => {}).
     *
     * Both hooks are wrapped in try/catch — never crash the host.
     */
    
    import { resolve } from "node:path"
    import { existsSync, readFileSync } from "node:fs"
    
    /**
     * Creates and returns system.transform and event hooks for session start.
     * @param $      OpenCode bash executor (tagged template)
     * @param root   package root directory
     * @param client OpenCode client (for tui.showToast)
     */
    export function createSessionStartHooks($: any, root: string, client: any) {
      let firstTransform = true
      let toastShown = false
    
      function computeTarget(): string {
        const home = process.env.HOME || process.env.USERPROFILE || "/tmp"
        const cwd = process.cwd()
        const projectJson = resolve(cwd, "opencode.json")
        const projectJsonc = resolve(cwd, "opencode.jsonc")
        if (existsSync(projectJson) || existsSync(projectJsonc)) {
          return resolve(cwd, ".opencode")
        }
        return resolve(home, ".config", "opencode")
      }
    
      function readUpdateSummary(): string | null {
        const target = computeTarget()
        const f = resolve(target, ".engram-update.jsonc")
        if (!existsSync(f)) return null
        try {
          const m = JSON.parse(readFileSync(f, "utf-8"))
          if (m.state === "in_progress") {
            return "Update partially applied. Run /engram-update to continue"
          }
          return "Updates Engram Available!\nRun /engram-update"
        } catch {
          return null
        }
      }
    
      return {
        async "experimental.chat.system.transform"(_input: any, output: { system: string[] }) {
          try {
            if (!firstTransform) return
            firstTransform = false
    
            const engramPy = resolve(root, "scripts", "engram.py")
            const result = await $`python3 ${engramPy} session-start`.nothrow().quiet()
            const nudge = result.stdout.toString().trim()
            if (nudge) output.system.push(`\n[engram] ${nudge}`)
    
            const updateSummary = readUpdateSummary()
            if (updateSummary) {
              output.system.push(`\n[engram] ${updateSummary}`)
            }
          } catch {}
        },
    
        async event(input: { event: any }) {
          try {
            if (toastShown) return
            if (input.event.type !== "session.idle") return
    
            const updateSummary = readUpdateSummary()
            if (!updateSummary) return
    
            toastShown = true
            client.tui.showToast({
              body: { title: "Engram", message: "Updates Engram Available!\nRun /engram-update", variant: "info", duration: 30000 },
            }).catch(() => {})
          } catch {}
        },
      }
    }
    
  • hooks/shell-env.tsGitHub
    Read the script
    /**
     * Engram — Shell Environment Hook
     * ================================
     *
     * Injects OPENCODE_PLUGIN_ROOT and ENGRAM_ROOT into every shell execution.
     * Resolves to the extracted .opencode/ target if engram.py exists there,
     * falling back to the npm package root (pre-extract).
     *
     * Also forwards ENGRAM_HOME and ENGRAM_TODAY from the process environment
     * to child shells (used by the engine for state isolation and time-travel tests).
     */
    
    import { existsSync } from "node:fs"
    import { resolve } from "node:path"
    
    /**
     * Creates the shell.env hook. Injects ENGRAM_ROOT and OPENCODE_PLUGIN_ROOT
     * at every shell execution.
     * @param packageRoot npm cache path (fallback when not yet extracted)
     */
    export function createShellEnvHook(packageRoot: string) {
      return {
        async "shell.env"(input: any, output: { env: Record<string, string> }) {
          try {
            const cwd = input.cwd || process.cwd()
            const target = extractTarget(cwd)
            const pluginRoot = existsSync(resolve(target, "scripts", "engram.py")) ? target : packageRoot
    
            output.env["ENGRAM_ROOT"] = pluginRoot
            output.env["OPENCODE_PLUGIN_ROOT"] = pluginRoot
            if (process.env.ENGRAM_HOME) output.env["ENGRAM_HOME"] = process.env.ENGRAM_HOME
            if (process.env.ENGRAM_TODAY) output.env["ENGRAM_TODAY"] = process.env.ENGRAM_TODAY
          } catch {}
        },
      }
    }
    
    function extractTarget(cwd: string): string {
      const home = process.env.HOME || process.env.USERPROFILE || "/tmp"
      const projectJson = resolve(cwd, "opencode.json")
      const projectJsonc = resolve(cwd, "opencode.jsonc")
      if (existsSync(projectJson) || existsSync(projectJsonc)) {
        return resolve(cwd, ".opencode")
      }
      return resolve(home, ".config", "opencode")
    }
    

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

@nagisanzenin
Ships withengram

Evidence-based learning engine — first-principles curricula, free-recall verification with receipts, FSRS-scheduled memory, and explorable artifacts. Learn anything; keep it.

1.5k3 skills3 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
1.5k
Star history of nagisanzenin/engram on GitHub, a chart by star-history.com

More plugins that ship hooks.

i-have-adhd

i-have-adhd

A skill that shapes Claude Code's output for readers with ADHD: it leads with the next action, numbers multi-step work, restates state across turns, drops tangents, gives specific time estimates, and makes progress visible. Invoke it with /i-h.

1 skill1 hook60k
@ayghri@ayghri
Install Plugin
pua

pua

A Claude Code plugin that makes the assistant work harder when it stalls or repeats failures. It adds pressure-style coaching, plus skills for workplace reminders and senior engineer, CTO and team-lead modes for planning and execution.

12 skills+320k
@tanweai@tanweai
Install Plugin
claude-obsidian

claude-obsidian

Turns the sources you read, such as papers, articles and transcripts, into a linked wiki of Markdown notes in Obsidian. Claude keeps sources and citations so every summary can be traced back to its origin.

15 skills+216k
@agricidaniel@agricidaniel
Install Plugin
obsidian-second-brain

obsidian-second-brain

Gives Claude Code a persistent second brain stored as plain markdown in an Obsidian vault. Notes rewrite themselves as new sources arrive, and scheduled agents handle research synthesis and note capture.

1 skill+25.3k
@eugeniughelbur@eugeniughelbur
Install Plugin
arscontexta

arscontexta

A Claude Code plugin that builds a personal knowledge system through conversation, with skills for adding knowledge domains, checking vault health, getting methodology guidance, and evolving the system over time.

10 skills+23.5k
@agenticnotetaking@agenticnotetaking
Install Plugin
pro-workflow

pro-workflow

A Claude Code workflow plugin that learns from your corrections and keeps a self-correcting memory across sessions. It adds skills for agent teams, parallel worktree batch changes, bug capture as GitHub issues, project auto-setup and context compaction.

41 skills+32.9k
@rohitg00@rohitg00
Install Plugin
hey

hey

A command line tool for HEY that lets you read and send email, manage contacts, labels, calendars, todos, habits, time tracking and journal entries from the terminal or from your coding agent.

1 skill1 hook423
@basecamp@basecamp
Install Plugin
basecamp-cli

basecamp-cli

Lets you manage Basecamp projects, todos, cards, messages, files and schedules from your terminal or from Claude Code, so you stop switching to the web app for routine updates.

3 skills4 hooks294
@basecamp@basecamp
Install Plugin

Questions about the hooks in engram.

Which coding agents can use the hooks in engram?
The hooks in engram install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.