Skip to content
Productivity
Hook

Hooks

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

From plugin
hush
541 command8 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.

  • node

PreToolUse

  • Matches^(Bash|PowerShell)$node

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • node

SubagentStart

  • node

PreCompact

  • node

PostCompact

  • node

SessionEnd

  • node

PostToolUse

  • Matches^(Bash|PowerShell|Read|Grep)$node
  • node
Read hooks/hooks.json

Where it lives

  • hooks/compress-tool-output.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    // PostToolUse hook: mechanically shrinks Bash/PowerShell output — plus Read
    // results for log-shaped and machine-generated files, hush's own recovery
    // files, and oversized Grep match lists — before they enter context.
    // Deterministic text transforms only — no heuristic ever touches failure
    // detail: failing runs get a much larger cap and everything kept is verbatim.
    
    const fs = require("fs");
    const os = require("os");
    const path = require("path");
    const { readInput, emitToolOutput, decodeResponse, SHELL_FIELDS, lastUserPromptText } = require("./lib/harness");
    const { safeWriteFileSync } = require("./lib/safe-write");
    const { combineActions, buildRecord, recoveryGap, sizeGap, fieldGap, debugManifestPath, appendRecord } = require("./lib/transform-manifest");
    const sidecarStore = require("./lib/sidecar-store");
    const { coreOff, isOff } = require("./lib/gate");
    
    const WATCHED_TOOLS = new Set(["Bash", "PowerShell", "Read", "Grep"]);
    
    // Caps are in lines. Passing output is mostly noise (install trees, progress
    // logs); failing output is evidence, so it keeps ~4x more.
    const CAP_PASS = intEnv("HUSH_CAP_PASS", 60);
    const CAP_FAIL = intEnv("HUSH_CAP_FAIL", 250);
    // Enumeration carve-out cap (see requestsEnumeration). Large enough that a
    // normal noisy build/log passes whole — no omission markers at all — so a model
    // asked to report EVERY item has nothing elided to distrust. Still bounded, so
    // a pathological megaline dump can't blow context.
    const CAP_ENUMERATE = 2000;
    // Grep content-mode results below this size pass whole; above it, each
    // matched file keeps its first few match lines and the rest collapse to a
    // per-file count (compressGrep). A small result costs little to send whole,
    // and per-file counts keep the file map intact. compress() uses the same floor
    // for same-shape collapse (collapseTemplates) and for the line cap (capLines).
    const GREP_MIN_CHARS = 4000;
    const GREP_KEEP_PER_FILE = 3;
    
    function intEnv(name, fallback) {
      const n = parseInt(process.env[name] || "", 10);
      return Number.isFinite(n) && n > 0 ? n : fallback;
    }
    
    // eslint-disable-next-line no-control-regex
    const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
    
    function stripAnsi(text) {
      return text.replace(ANSI_RE, "");
    }
    
    // Progress bars redraw via a bare \r (no following \n); only the final state
    // of each physical line matters. \r\n is an ordinary Windows line ending, not
    // a redraw — normalize it away first or every CRLF-terminated line (i.e.
    // nearly all native Windows console output) collapses to empty.
    function resolveCarriageReturns(text) {
      return text
        .replace(/\r\n/g, "\n")
        .split("\n")
        .map((line) => {
          const i = line.lastIndexOf("\r");
          return i === -1 ? line : line.slice(i + 1);
        })
        .join("\n");
    }
    
    // Keep lines (isKeepLine) never join a repeat run: six identical
    // "ERROR: connection refused" lines are six failures, and folding them into
    // one line plus a count contradicts what the capped-failure footer promises
    // about keeping every failure line in original order.
    function dedupeConsecutive(lines) {
      const out = [];
      let run = 0;
      for (let i = 0; i <= lines.length; i++) {
        if (i < lines.length && out.length && lines[i] === out[out.length - 1] && lines[i].trim() !== "" && !isKeepLine(lines[i])) {
          run++;
          continue;
        }
        if (run > 0) out.push(`[hush: previous line repeated ${run}x]`);
        run = 0;
        if (i < lines.length) out.push(lines[i]);
      }
      return out;
    }
    
    // Real logs repeat the same SHAPE far more than they repeat identical lines
    // (dedupeConsecutive only catches the latter) — "INFO worker-3 processing job
    // 8841" x hundreds, each with a different id/timestamp. Collapsing those runs
    // compounds hush's strongest domain. Two lines "share a template" iff: same
    // token count; >=50% of positions token-identical; and >=2 of those identical
    // positions are "anchor" tokens (>=3 chars, no digits) — the anchor floor is
    // what stops two lines merging on a shared timestamp or short flag alone.
    // Comparison is always against the run's first line (its exemplar), so the
    // whole run stays anchored to one shape instead of drifting line to line.
    const TEMPLATE_MIN_RUN = 5;
    
    function templateTokens(line) {
      return line.trim().split(/\s+/).filter(Boolean);
    }
    
    function isAnchorToken(tok) {
      return tok.length >= 3 && !/\d/.test(tok);
    }
    
    function shareTemplate(aTokens, bTokens) {
      if (!aTokens.length || aTokens.length !== bTokens.length) return false;
      let same = 0;
      let anchors = 0;
      for (let i = 0; i < aTokens.length; i++) {
        if (aTokens[i] === bTokens[i]) {
          same++;
          if (isAnchorToken(aTokens[i])) anchors++;
        }
      }
      return same / aTokens.length >= 0.5 && anchors >= 2;
    }
    
    // INVARIANTS of template collapse — what may be collapsed, and what never
    // may. Stated here because the view's own footer
    // (TEMPLATE_COLLAPSE_NOTE) states them to the model, and a promise the code
    // does not keep is worse than no promise:
    //
    //   1. Only a line that shares its run exemplar's shape is ever dropped: same
    //      token count, >=50% of positions token-identical, >=2 identical anchor
    //      tokens (shareTemplate). The exemplar itself is always kept verbatim.
    //   2. A keep line (isKeepLine — warning/error/failure/deprecation/critical) is
    //      never collapsed — it never joins a run and always breaks one. Over-normalizing
    //      distinct errors into one exemplar is the known failure mode this
    //      sidesteps entirely, rather than trying to tune around it.
    //   3. A line naming a prompt-quoted identifier is never collapsed either, on
    //      the same terms capLines and compressGrep use it: high-precision spans
    //      only, and a span matching more than RELEVANCE_COMMON lines is dropped as
    //      too common to discriminate.
    //   4. A line that names a failing go test (`=== RUN` and the like, see
    //      goFailureIdx) is never collapsed, so capLines can still tell whic
  • hooks/postcompact-rearm.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    // PostCompact hook: re-arms the once-per-session marker-provenance note after
    // compaction. compress-tool-output.js's note fires once per session, guarded
    // by a sentinel file (hush-note in the session's sidecar directory) — but compaction
    // summarizes the note away while the sentinel still says "delivered", so
    // markers appearing after compaction arrive unexplained and risk being read
    // as prompt injection. Deleting the sentinel re-arms delivery on the next
    // marker, and is harmless if the file never existed.
    //
    // Emits nothing: re-injecting the note unconditionally on every compaction
    // would spend tokens on sessions that never emit another marker. Silence is
    // the design — the existing marker-triggered path re-delivers the note only
    // when a marker is actually about to be shown.
    
    const { readInputOrNull: readInput } = require("./lib/harness");
    const fs = require("fs");
    const { notePath } = require("./lib/sidecar-store");
    const { coreOff } = require("./lib/gate");
    
    // Re-arming is deletion, and deletion is total: the note sentinel is dropped
    // so the next compaction can claim it again, never carried forward as still
    // live. Nothing here re-arms per entry, and nothing here trusts state content.
    //
    // session_id arrives from stdin raw; sidecar-store's sessionDir flattens it to
    // one path segment, so the sentinel path cannot leave the sidecar root — a
    // traversal-shaped id names a directory hush owns, never someone else's file.
    function unlinkSentinels(sessionId) {
      try {
        fs.unlinkSync(notePath(sessionId));
      } catch {
        /* ENOENT fine; anything else is not worth breaking a session over */
      }
    }
    
    function main() {
      try {
        if (coreOff()) return;
        const data = readInput();
        if (data === null) return; // malformed stdin
        if (typeof data.session_id !== "string" || !data.session_id) return;
        unlinkSentinels(data.session_id);
      } catch {
        /* fail-open: never break a session over re-arming a note */
      }
    }
    
    if (require.main === module) main();
    
    module.exports = { readInput, unlinkSentinels };
    
  • hooks/precompact-summary.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    // PreCompact hook: shapes the compaction summarizer's own instructions — the
    // one recurring payload hush's PostToolUse compression can never touch,
    // since a summary replaces prior messages and is re-sent on every later API
    // call. Claude Code builds these instructions from this hook's RAW STDOUT
    // (trimmed, newline-joined across all PreCompact hooks), not from
    // hookSpecificOutput JSON — so this prints plain text only, and always
    // exits 0 even on failure (fail-open: a hush crash must never break a
    // session, especially not at the one moment a summary is about to replace
    // the conversation).
    //
    // Two blocks, both format-shaping only — never information-dropping:
    // (a) a static directive: structured list, preserve every path/identifier/
    //     decision/error verbatim, drop narration and tool-output restatement.
    // (b) only when this session has recovery files on disk: their paths, so the
    //     summary can carry the reference instead of the content. The session's
    //     sidecar directory IS the registry of what is still live (see
    //     lib/sidecar-store.js) — every listed path is stat-verified at summary
    //     time, and a listing longer than the cap says how many it left out
    //     instead of dropping them silently.
    
    const { readInputOrNull: readInput, emitRaw } = require("./lib/harness");
    const fs = require("fs");
    const path = require("path");
    const { sessionDir } = require("./lib/sidecar-store");
    const { coreOff, isOff } = require("./lib/gate");
    
    const SIDECAR_CAP = 20;
    
    const STATIC_BLOCK =
      "Summary format: a compact structured list, not prose. Preserve verbatim every file path, " +
      "identifier, command, version number, error message, decision, and open thread — losing one " +
      "forces re-exploration that costs more than the summary saves. Drop narration, pleasantries, " +
      "step-by-step retellings, and content restated from tool outputs. One fact per line.";
    
    // This session's recovery files only — its own directory under the sidecar
    // root — forward-slashed, sorted, stat-verified, or null when there's nothing
    // to point at. These paths stay valid across the compaction this hook is
    // announcing: sidecars are removed at session end, never at compaction.
    //
    // Verified means the entry is a regular file at summary time: a name from the
    // listing that has since been deleted, or that is a directory, is not a live
    // recovery artifact and must not be handed to the summarizer as one.
    //
    // What a directory listing knows: which artifacts are live, and how many.
    // What it does NOT know: which tool produced each one, or whether a given
    // file backs a digest (a long output parked whole) or a collapsed match list
    // (the complete matches parked whole) — both are content-hash names in the
    // same directory, and no per-file metadata is persisted (manifest records are
    // HUSH_DEBUG-only by design). So the block names both shapes rather than
    // claiming one per path.
    function liveSidecarFiles(dir) {
      let names;
      try {
        names = fs.readdirSync(dir);
      } catch {
        return []; // no directory: this session has parked nothing
      }
      return names
        .filter((f) => f.endsWith(".txt")) // .tmp partials from an interrupted write are not artifacts
        .sort() // stable order, so repeated compactions in one session list the same files the same way
        .filter((f) => {
          const st = fs.statSync(path.join(dir, f), { throwIfNoEntry: false });
          return !!st && st.isFile();
        });
    }
    
    function buildSidecarBlock(sessionId) {
      if (typeof sessionId !== "string" || !sessionId) return null;
      const dir = sessionDir(sessionId);
      const live = liveSidecarFiles(dir);
      if (!live.length) return null;
      const paths = live.slice(0, SIDECAR_CAP).map((f) => path.join(dir, f).replace(/\\/g, "/"));
      // Over SIDECAR_CAP the remainder is named by count and directory rather
      // than path — the summary stays bounded and the drop is explicit.
      const rest = live.length - paths.length;
      const tail = rest > 0 ? ` and ${rest} more in ${dir.replace(/\\/g, "/")}` : "";
      return (
        `Recovery files this session parked on disk — the conversation shows only a shortened view of ` +
        `each, either a digest of a long output or a collapsed list of matches: ${paths.join(", ")}${tail}. ` +
        `Keep these paths in the summary; do not reproduce their content. Reading one back returns the ` +
        `detail its shortened view left out. If a file is gone, re-running a search over unchanged files ` +
        `reproduces its matches; no other command is guaranteed to produce the same output twice.`
      );
    }
    
    function main() {
      try {
        if (coreOff()) return;
        if (isOff("HUSH_COMPACT")) return;
        const data = readInput();
        if (data === null) return; // malformed stdin
    
        const blocks = [STATIC_BLOCK];
        const sidecarBlock = buildSidecarBlock(data.session_id);
        if (sidecarBlock) blocks.push(sidecarBlock);
        emitRaw(blocks.join("\n\n"));
      } catch {
        /* fail-open: never break a session over a summary hint */
      }
    }
    
    if (require.main === module) main();
    
    module.exports = { STATIC_BLOCK, buildSidecarBlock, liveSidecarFiles, readInput, SIDECAR_CAP, sessionDir };
    
  • hooks/preserve-exit-code.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    // CLAUDE-CODE-ONLY WORKAROUND. This whole hook exists because of one detail
    // of one host: a non-zero exit routes to PostToolUseFailure, which has no
    // rewrite channel. A harness that lets a failing command's output be rewritten
    // needs none of this, and a port should expect the file to be dead code rather
    // than translate it. Everything host-shaped it does go through
    // lib/harness.js; what stays here is the shell wrapping itself.
    //
    // PreToolUse hook: wraps Bash/PowerShell commands so a non-zero exit never
    // reaches Claude Code as a TOOL failure. A command that "fails" in the shell
    // sense (build broke, tests red) still ran successfully as far as the Bash/
    // PowerShell tool itself is concerned — but Claude Code routes a non-zero
    // exit through PostToolUseFailure, an event with no mechanism to shrink
    // content (no `updatedToolOutput`, unlike PostToolUse). That silently
    // defeats compress-tool-output.js's CAP_FAIL path on exactly the noisy-
    // failure case hush's compression exists for: hush's PostToolUse hook is
    // never invoked for a command that exits non-zero, so a huge failing build
    // or test dump reaches context uncompressed and stays that way for the rest
    // of the session.
    //
    // Wrapping forces the tool call itself to always report success (so
    // PostToolUse fires, where compression works), while the real exit code
    // survives as a trailer marker compress-tool-output.js reads authoritatively
    // (see extractWrappedExit there) instead of guessing from response shape/regex.
    // It reads only a marker that closes the output of a command carrying this
    // wrapper, so the trailer must stay the last thing the wrapped command prints.
    
    const { readInput, emitUpdatedInput } = require("./lib/harness");
    const { coreOff, isOn } = require("./lib/gate");
    
    const WATCHED_TOOLS = new Set(["Bash", "PowerShell"]);
    const MARKER_PREFIX = "[[hush:exit=";
    const MARKER_SUFFIX = "]]";
    
    function alreadyWrapped(command) {
      return typeof command === "string" && command.includes(MARKER_PREFIX);
    }
    
    // Deliberately three separate statements with no `$var` ever inside a quoted
    // string, and no parentheses around a variable — confirmed live against a
    // real session that BOTH of the more natural forms get rejected outright by
    // Claude Code's own command-safety layer before the command ever runs:
    // `Write-Output "...$LASTEXITCODE..."` -> "Command contains expandable
    // strings with embedded expressions"; `Write-Output ("..." + $LASTEXITCODE +
    // "...")` -> "Command contains subexpressions $()" (parens near a variable
    // read the same as a subexpression to that checker, even though this isn't
    // one). Single-quoted literals plus a bare `$LASTEXITCODE` expression
    // statement (PowerShell auto-prints an unconsumed expression's value) is the
    // most primitive construct that still gets through, and it does — verified
    // live, exit code correctly reported on its own line, tool succeeds.
    //
    // The command runs inside `& { ... } | Out-String` rather than bare — found
    // live: a cmdlet pipeline ending in something like `Select-Object` (no
    // explicit `Format-Table`/`Out-*`) defers rendering to PowerShell's implicit
    // end-of-pipeline auto-formatter, which buffers objects to compute column
    // widths before emitting anything.
    // Appending our trailer statements — and then a hard `exit 0` — moves
    // execution past that pipeline before the deferred formatter flushes,
    // silently swallowing ALL of the command's output, not just ours.
    // Reproduced directly: bare `Get-ChildItem` through the old wrapper worked;
    // `Get-ChildItem | Select-Object Name` produced nothing; adding
    // `Format-Table -AutoSize` back made it work again. `Out-String` forces
    // full, synchronous rendering of whatever the block produces — object
    // output or plain text alike — before the next statement runs, so nothing
    // is left buffered when `exit 0` fires. `-Width` is set explicitly wide:
    // Out-String's default wraps to the host's console width (often 80 in a
    // non-interactive host), which would otherwise hard-wrap ordinary build/test
    // output into extra lines and corrupt hush's line-based compression.
    function wrapPowerShell(command) {
      return (
        `& { ${command} } 2>&1 | Out-String -Width 4096\n` +
        `Write-Output '${MARKER_PREFIX}'\n$LASTEXITCODE\nWrite-Output '${MARKER_SUFFIX}'\nexit 0`
      );
    }
    
    // Same shape for consistency, and to avoid relying on bash's own
    // "$var"-in-quotes interpolation in case an analogous check applies there:
    // single-quoted literals around a bare (unquoted, safe for a plain integer)
    // variable reference.
    function wrapBash(command) {
      return `${command}\n__hush_exit=$?\necho '${MARKER_PREFIX}'\necho $__hush_exit\necho '${MARKER_SUFFIX}'\nexit 0`;
    }
    
    // Wrapping is gated on the session's permission mode. Claude Code applies
    // PreToolUse updatedInput BEFORE permission evaluation, and its permission
    // engine both statically analyzes the rewritten command and splits it into
    // per-statement operations that must each match an allow rule. The trailer
    // cannot survive that under scoped allow rules on either shell, as of
    // Claude Code 2.1.207 with `Bash(node*)`/`PowerShell(node*)` rules:
    //   - PowerShell: a `& {` opening makes the first AST element a script
    //     block ("Command name is a dynamic expression"), and even with it
    //     removed the trailer's `$LASTEXITCODE` and `exit 0` statements are
    //     unapproved operations. No variable-free way to emit an exit code
    //     exists, so no trailer form can pass.
    //   - Bash: the trailer's `$?` / `$__hush_exit` expansions are rejected
    //     outright ("Contains simple_expansion") — and that check runs before
    //     rule matching, so it denied EVERY wrapped command, even ones an
    //     allow rule covered.
    // Under `bypassPermissions` none of that machinery runs (the full
    // PowerShell wrapper executes and the marker comes back), so wrapping is
    // safe exactly there. `HUSH_WRAP=1` opts back in for sessions
    // whose rules a
  • hooks/session-end-cleanup.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    // SessionEnd hook: sidecar files are session-scoped, so this is where they go.
    // The session's own directory is deleted outright — parked copies, saved.json
    // and the once-per-session note sentinel alike; anything a crashed session
    // left behind is caught by the age-graced sweep, which never touches a
    // directory a live session has written to recently.
    //
    // Deletion happens only at session end, never at compaction: the PreCompact
    // summary hands the model those exact paths, and a within-session compaction
    // must leave every one of them readable.
    //
    // Emits nothing — cleanup is a side effect, and SessionEnd output has nowhere
    // to land. Always exits 0: a session that is already over must not be handed
    // an error, and a sidecar left on disk costs nothing but temp space.
    
    const { readInputOrNull: readInput } = require("./lib/harness");
    const { removeSession, sweepStale } = require("./lib/sidecar-store");
    const { coreOff } = require("./lib/gate");
    
    function main() {
      try {
        if (coreOff()) return;
        const data = readInput();
        if (data === null) return; // malformed stdin
        if (typeof data.session_id === "string" && data.session_id) removeSession(data.session_id);
        sweepStale();
      } catch {
        /* fail-open: never break a session over cleanup */
      }
    }
    
    if (require.main === module) main();
    
    module.exports = { readInput };
    
  • hooks/silence-nudge.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    // Re-states the silence rule from a hook channel, next to the text the model
    // is about to write. The output style alone does not hold mid-turn silence on
    // the larger models; the same rule delivered here, where the model is about to
    // write, does.
    //
    // Two levels, picked by HUSH_NUDGE:
    //
    //   (default)  One reminder at the top of the turn, plus a corrective one
    //              fired ONLY when the transcript shows a mid-turn assistant
    //              text block this turn, at most once per turn. A turn that
    //              stays silent gets nothing mid-turn at all.
    //   max        A reminder on every tool result (doubled), plus the one at
    //              the top of the turn.
    //
    // A turn the harness opened (a task notification or a scheduled wakeup) gets
    // the AUTO reminder at its top instead of the turn text, at either level.
    // HUSH_NUDGE=off and HUSH_QUIET=off stop all of these reminders.
    //
    // Why the default reminds reactively instead of on every tool result: every
    // reminder is context the session carries and pays for, and `max` adds two on
    // every tool result whether or not anything slipped. The default adds one
    // only after a mid-turn text block shows up, so a session that stays quiet
    // pays nothing extra, and the corrective lands right where the turn-top
    // reminder has faded. Older values `turn`, `lean`, and `react` are accepted
    // as synonyms for the default, so nothing anyone set ever breaks.
    //
    // Positive-forward wording only, at every level. Naming the unwanted
    // behavior primes it — a clause that describes narrating produces narrating.
    
    const fs = require("node:fs");
    const path = require("node:path");
    const { quietOff, isOff } = require("./lib/gate");
    const { sessionDir } = require("./lib/sidecar-store");
    const { readInputAsync, emitContext, readTailLines, isRealUserPrompt, isTurnStart } = require("./lib/harness");
    
    const nudgeEnv = String(process.env.HUSH_NUDGE || "").trim();
    const OFF = isOff("HUSH_NUDGE");
    const MAX_MODE = /^max$/i.test(nudgeEnv);
    
    // max's own wording. With a step reminder beside it, "until the work is done"
    // holds better than the closed-boundary text below. Each text belongs to its
    // own configuration; the two are not interchangeable.
    const TURN =
      "hush: this turn is silent until the work is done; when Claude Code asks for an update, one short line beside your next tool call answers it. Everything you learn goes in the final message.";
    const STEP =
      "hush: your next output is a tool call. The final message is the only place you explain anything.";
    const TOOL = `${STEP} ${STEP}`;
    
    // The default's turn text. Closes a boundary TURN leaves open: "until the
    // work is done" let the model call the work done and announce a verification
    // step out loud, mid-turn, right before running it. It is the text for the
    // configuration with no standing step reminder. Its opening-line rule is the
    // shipped voice's own words (output-styles/hush.md), so the two agree;
    // tests/silence_nudge.test.js holds that sentence and checks both carry it,
    // so a rewording changes all three together.
    const TURN_DIAL =
      "hush: this turn opens with a tool call. If a line does come first, it answers three things in one breath: what you will do, what you do not know yet, and how you will find out. Then the turn is silent until the final message; when Claude Code asks for an update, one short line beside your next tool call answers it. Everything you learn goes in the final message.";
    
    // A turn the harness opened (a task notification or a ScheduleWakeup firing)
    // gets this instead of the turn text: "Everything you learn goes in the final
    // message" made every such turn end with a full status message. The rule
    // sentences are the shipped voice's own (output-styles/hush.md);
    // tests/silence_nudge.test.js holds them and checks both carry them.
    const AUTO =
      "hush: the harness started this turn, not the user. When such a turn brings nothing new, end it with no message or one short line. A failure, a blocker, a finished run or a pending decision still gets the full message.";
    
    // True when the transcript entry carrying this prompt is one the harness
    // wrote. UserPromptSubmit fires for those turns too, and its payload does not
    // say who opened the turn; the entry's origin does, read by isRealUserPrompt.
    // The entry is found by its text among the turn-start entries (isTurnStart),
    // newest first. No match, an unreadable transcript or a non-string prompt
    // reads as a typed turn, which keeps the existing reminder.
    function isAutomatedTurn(transcriptPath, prompt) {
      if (!transcriptPath || typeof prompt !== "string" || !prompt.trim()) return false;
      let lines;
      try {
        lines = readTailLines(transcriptPath);
      } catch {
        return false;
      }
      for (let i = lines.length - 1; i >= 0; i--) {
        let e;
        try {
          e = JSON.parse(lines[i]);
        } catch {
          continue;
        }
        if (!isTurnStart(e)) continue;
        const c = e.message && e.message.content;
        if (typeof c === "string" && c.trim() === prompt.trim()) return !isRealUserPrompt(e);
      }
      return false;
    }
    
    // The default's corrective state: nonzero once this turn's corrective has
    // fired. Lives beside the session's other scratch, so Core's session-end
    // cleanup clears it; with Core off nothing reaps it and it is left for OS
    // temp cleaning. Fail-open in the cheap direction — an unreadable transcript
    // or counter means no injection.
    function reactFile(sessionId) {
      return path.join(sessionDir(sessionId), "react-count");
    }
    function resetReact(sessionId) {
      try {
        fs.mkdirSync(sessionDir(sessionId), { recursive: true });
        fs.writeFileSync(reactFile(sessionId), "0");
      } catch { /* fail open */ }
    }
    // Claude Code's own request for a status line ("The user hasn't heard from
    // you in a while…"), recorded in the transcript as an attachment entry.
    const HOST_STATUS = "silent_turn_reminder";
    
    // Count assistant text blocks since the last real human prompt or the
    //
  • hooks/style-settings.jsGitHub
  • hooks/subagent-brief.jsGitHub

All 8 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

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.

@v-songbird
Ships withhush

Quieter sessions for Claude Code: less narration, shorter tool output and concise answers focused on the result.

Stars on GitHub

Momentum

stars in total
54
Star history of V-Songbird/hush 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
engram

engram

Helps you learn any topic with a first-principles curriculum, guided tutoring, and spaced review that schedules free-recall practice. Includes coaching for retention stats and study strategy.

3 skills+21.5k
@nagisanzenin@nagisanzenin
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

Questions about the hooks in hush.

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