Skip to content
Productivity
Hook

Hooks

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

From plugin
nagisanzenin-engram
1.4k3 skills3 agents1 hook
Install
> /plugin marketplace add nagisanzenin/engram
> /plugin install engram@engram

Ships with nagisanzenin-engram. Installing the plugin gets these hooks.

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.

Ships withnagisanzenin-engram

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

Get the whole plugin
Stats
1,426
Stars
103
Forks
Active
Maintenance
Python
Language
MIT
License
26d ago
Last commit
2mo ago
Created

Repo: nagisanzenin/engram