Hooks
What hush runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
$ npx -y skills add V-Songbird/hush --agent claude-codeShips with hush. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
^(Bash|PowerShell)$node "${CLAUDE_PLUGIN_ROOT}/hooks/preserve-exit-code.js"
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 "${CLAUDE_PLUGIN_ROOT}/hooks/silence-nudge.js"
SubagentStart
node "${CLAUDE_PLUGIN_ROOT}/hooks/subagent-brief.js"
PreCompact
node "${CLAUDE_PLUGIN_ROOT}/hooks/precompact-summary.js"
PostCompact
node "${CLAUDE_PLUGIN_ROOT}/hooks/postcompact-rearm.js"
SessionEnd
node "${CLAUDE_PLUGIN_ROOT}/hooks/session-end-cleanup.js"
PostToolUse
- Matches
^(Bash|PowerShell|Read|Grep)$node "${CLAUDE_PLUGIN_ROOT}/hooks/compress-tool-output.js" node "${CLAUDE_PLUGIN_ROOT}/hooks/silence-nudge.js"
Where it lives
- hooks/compress-tool-output.jsRunsGitHub
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 } = 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). Corpus-measured: the mass is in the >=4KB // tail, and per-file counts keep the file map intact. 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. Fewer than TEMPLATE_MIN_RUN same-shape lines collapse to nothing at all; // the run is emitted verbatim. // // Anything outside 2-4 is fair game, and the dropped lines are NOT recoverable // from the view — only from the source, which is what the - hooks/postcompact-rearm.jsRunsGitHub
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.jsRunsGitHub
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 } = 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 (process.env.HUSH_COMPACT === "off") 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.jsRunsGitHub
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 EXIT_MARKER_RE there) instead of guessing from response shape/regex. const { readInput, emitUpdatedInput } = require("./lib/harness"); const { coreOff } = 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 — all // verified live against cli 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 (verified live: // 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 are blanket per-tool grants (plain `Bash` / `PowerShell`, // no command pattern) — those match the wrapped command as a whole; the // bu - hooks/session-end-cleanup.jsRunsGitHub
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.jsRunsGitHub
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: style wording changes measured flat, while the same rule // delivered here cut mid-turn narration by roughly 90%. // // Two levels, picked by HUSH_NUDGE — the default changed 2026-08-11: // // (default) One reminder at the top of the turn, plus a corrective one // fired ONLY when the transcript shows a new mid-turn assistant // text block this 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 — what shipped as the only mode through // 1.3.0. // // Why the default reminds reactively instead of on every tool result: a // reminder injected mid-turn does not survive a session resume byte-stably — // every resume re-writes several thousand cached tokens at cache-write // prices, no matter which hook fires it or how short it is. Reminding on // every tool result (`max`) cost 14-45% over no plugin on six long // engineering fixtures (Sonnet, 2026-08-08, n=12 per arm across several // batches). The reactive default pays that tax only in the sessions that // actually slip: measured across three independent batches (2026-08-11), it // came in cheaper than even the no-mid-turn design every time, with equal or // fewer mid-turn leaks — the corrective lands with maximum recency, right // where a turn-top reminder has decayed. 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, OFF_TOKEN } = require("./lib/gate"); const { sessionDir } = require("./lib/sidecar-store"); const { readInputAsync, emitContext, readTailLines, isRealUserPrompt } = require("./lib/harness"); const nudgeEnv = String(process.env.HUSH_NUDGE || "").trim(); const OFF = OFF_TOKEN.test(nudgeEnv); const MAX_MODE = /^max$/i.test(nudgeEnv); // max's own wording. Paired with a step reminder present, "until the work is // done" measured BETTER than the closed-boundary text below — the two texts // are proven in their own configuration only, not interchangeable. const TURN = "hush: this turn is silent until the work is done. 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. Measured cutting leaks // roughly in half against TURN in the configuration this text is used in — // no standing step reminder present. const TURN_DIAL = "hush: this turn is silent until the final message. It opens with a tool call, not a line about what you will look at. Everything you learn goes in the final message."; // The reminders re-state the quiet rule of whichever style holds hush's own // slot. Stock and every variant that passes scripts/verify-style.js carry this // phrase. A style activated there without it shares progress between tool // calls, and a reminder would only contradict it. An unreadable slot keeps the // reminders on. const QUIET_PHRASE = "Not one word between tool calls"; function styleKeepsQuiet(pluginRoot = path.join(__dirname, "..")) { try { return fs.readFileSync(path.join(pluginRoot, "output-styles", "hush.md"), "utf8").includes(QUIET_PHRASE); } catch { return true; } } // The default's corrective state: how many mid-turn text blocks have already // been answered with a reminder this turn. 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 */ } } // Count assistant text blocks since the last real human prompt — mid-turn // text, because the turn's own final message cannot exist yet while a // PostToolUse hook is firing. Fail-SILENT on any trouble: no count means no // injection, which is the cheap direction. function countMidTurnText(transcriptPath) { let lines; try { lines = readTailLines(transcriptPath); } catch { return 0; } let count = 0; for (let i = lines.length - 1; i >= 0; i--) { let e; try { e = JSON.parse(lines[i]); } catch { continue; } if (isRealUserPrompt(e)) break; if (e.type !== "assistant" || e.isSidechain) continue; const c = e.message && e.message.content; if (!Array.isArray(c)) continue; for (const b of c) { if (b.type === "text" && typeof b.text === "string" && b.text.trim()) count++; } } return count; } // Fires at most once per NEW text block: the reminder lands right after the // block that earned it, then stays quiet until another appears. function reactShouldFire(sessionId, transcriptPath) { try { const n = countMidTurnText(transcriptPath); if (n === 0) return false; const f = reactFile(sessionId); let seen = 0; try { seen = Number(fs.readFileSync(f, "utf8")) || 0; } catch { seen = 0; } if (n <= seen) return false; fs.mkdirSync(path.dirname(f), { recursive: true }); - hooks/subagent-brief.jsRunsGitHub
All 7 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.
Quieter sessions for Claude Code: less narration, shorter tool output and concise answers focused on the result.
Repo: V-Songbird/hush

