Skip to content
Code Generation
Hook

Hooks

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

From plugin
ripwire
2.4k19 skills5 hooks1 MCP
Install
$ npx -y skills add redhat-et/ripwire --agent claude-code

Ships with ripwire. Installing the plugin gets these hooks.

Where it lives

  • hooks/ripwire-claude-route.shGitHub
    Read the script
    #!/usr/bin/env bash
    # hooks/ripwire-claude-route.sh — OPT-IN Claude Code UserPromptSubmit router. Ask the deterministic
    # --help-task classifier BEFORE the first tool is chosen, inject ONE paste-ready command only when that
    # classifier says `recommend`, and instrument the decision without ever retaining prompt text. The
    # --observe arm is called by hooks/ripwire-nudge.sh on PreToolUse and closes the adoption-within-two
    # loop pre-registered in docs/EVALS.md §4. Advisory-only: any missing dependency or error degrades to
    # silence, never to a blocked prompt.
    #
    # WHY THIS EXISTS, AND WHY IT IS NOT THE RETIRED NUDGE WEARING A HAT (2026-09-02). The PreToolUse nudge
    # in hooks/ripwire-nudge.sh was measured inert by a randomized A/B and retired — see §RETIRED there and
    # the readout in docs/EVALS.md §4. Three things differ here, and they are the hypothesis under test:
    #   1. THE MOMENT. This runs before the agent has chosen a tool, not after it already reached for one.
    #   2. THE PAYLOAD. A runnable command with its arguments already filled in from the prompt, not a verb
    #      name and an ellipsis the agent has to finish.
    #   3. THE GATE. `--help-task` has a measured precision (1.000 / harmful 0.000 on its tuning corpus,
    #      test/taskroutecheck.sh), so this is silent on the prompts it cannot route rather than firing on
    #      everything. Silence is the common case and is not a failure.
    # The round cannot separate the three, and the registration says so.
    #
    # THE ARM IS THE METER'S ARM, NOT A SECOND COIN FLIP. It resolves `arm` exactly as meter_init() in
    # hooks/ripwire-nudge.sh does — env over `meter.conf` over the `auto` default (issue #381; it was `treatment`), with the literal
    # `auto` selecting the same stable session-id hash — so a session lands on the SAME side in both
    # instruments and the two logs join on session_hash. A control session runs the classifier, writes the
    # identical row and the identical pending file, and injects NOTHING; its adoption-within-two number is
    # "how often would the agent have run that verb anyway", which is the quantity the retired nudge never
    # had and the reason its readouts were uninterpretable.
    #
    # PROMPT-INJECTION POSTURE (a contract, gated by test/routehookcheck.sh). The injected context is
    # assembled from exactly two sources: the compile-time constant framing string below, and the
    # classifier's own XML, whose intent/skill/reason strings come from the binary's route table and whose
    # only variable parts are a symbol or literal taken FROM THE USER'S OWN PROMPT and XML-escaped by the
    # binary. NO REPOSITORY CONTENT REACHES THE OUTPUT — not a file name, not a match, not a body. The
    # whole thing is then passed through `jq --arg`, so a prompt carrying XML or JSON structure breakers
    # produces well-formed JSON with the breakers inert inside a string. A router steerable by the thing it
    # is reading would be a worse failure than a router that does not work.
    #
    # NEVER EXIT 2. For UserPromptSubmit, exit 2 BLOCKS the prompt and erases it. This hook is advisory and
    # has no business doing that under any condition, including its own bugs — every path below exits 0.
    #
    # Contract (verified against https://code.claude.com/docs/en/hooks, 2026-09-02): UserPromptSubmit JSON
    # arrives on stdin with {session_id, transcript_path, cwd, permission_mode, hook_event_name, prompt}.
    # Advisory context = exit 0 plus this JSON on stdout:
    #   {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}}
    # Plain stdout would also be added as context; JSON is used because it is unambiguous and because the
    # codex sibling already emits this exact shape.
    set -u
    
    command -v jq >/dev/null 2>&1 || exit 0
    input="$( cat )" || exit 0
    
    meter_home()
    {
        [ "${RIPWIRE_ROUTE_METER:-1}" != 0 ] || return 1
        meterHome="${RIPWIRE_HOME:-${HOME:+$HOME/.ripwire}}"
        [ -n "$meterHome" ] || return 1
        mkdir -p "$meterHome/routing-pending" 2>/dev/null || return 1
        routingLog="$meterHome/routing.jsonl"
        # A session that ends before two ripwire calls leaves its pending file behind; expire the strays so
        # routing-pending/ never accumulates unboundedly. Best-effort, like everything else here.
        find "$meterHome/routing-pending" -type f -name '*.json' -mtime +7 -delete 2>/dev/null || true
    }
    
    hash_text()
    {
        printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1
    }
    
    # ---- resolve_arm SESSION — the meter's arm resolution, reimplemented here rather than sourced.
    #      hooks/ripwire-nudge.sh is a hook, not a library: sourcing it would run its whole PreToolUse path.
    #      The rules are the ones meter_init() applies and they must not drift — `control` and `treatment`
    #      force an arm, `auto` selects the stable cksum split (low two decimal digits, <50 is control), and
    #      ANY other value, including a hash that does not come back as a plain integer, reads as
    #      `treatment`. That last clause is why a broken `cksum` cannot silently invent a third population.
    route_arm="treatment"
    resolve_arm()
    {
        _ra_conf=""
        _ra_home="${RIPWIRE_HOME:-${HOME:+$HOME/.ripwire}}"
        if [ -n "$_ra_home" ] && [ -f "$_ra_home/meter.conf" ]
        then
            # `|| [ -n "$_ra_k" ]` keeps a final line that has no newline (read returns 1 on it but has filled
            # the variables); `${_ra_v%$'\r'}` drops the CR of a CRLF file so `arm=control\r` reads as `control`.
            while IFS='=' read -r _ra_k _ra_v || [ -n "$_ra_k" ]
            do
                _ra_v="${_ra_v%$'\r'}"
                case "$_ra_k" in arm) _ra_conf="$_ra_v" ;; esac
            done < "$_ra_home/meter.conf"
        fi
        # UNSET (no env, no `arm=` in meter.conf) is `auto`, not `treatment` (issue #381): the shipped default
        # used to put every session on treatment, so the pre-registered treatment-minus-control difference could
        # never be computed. An explicit `treatment` stays the opt-out; any OTHER value still reads as treatment.
        case "${RIPWIRE_METER_ARM:-${_ra_conf:-auto}}" in
            control) route_arm="control
  • hooks/ripwire-claude-toolroute.shGitHub
    Read the script
    #!/usr/bin/env bash
    # hooks/ripwire-claude-toolroute.sh — OPT-IN Claude Code PreToolUse SECOND ROUTER ARM, pre-registered
    # in docs/EVALS.md ("A second router arm — route on the agent's FIRST TOOL CALL, not the prompt",
    # 2026-09-03). Routes on the SHAPE of the tool call itself rather than the prompt that led to it:
    # a recursive grep/rg on a source path names `--grep=<pattern>`; a Read of a source file names
    # `--expand=<symbol>` (only when the symbol resolves on the root) or `--for` with the file already
    # known. Advisory-only, exactly like hooks/ripwire-claude-route.sh (the prompt arm) and the retired
    # hooks/ripwire-nudge.sh nudge tiers: never blocks, never denies, degrades to silence on any error.
    #
    # WHY A SECOND ARM. hooks/ripwire-claude-route.sh's own instrument (docs/EVALS.md §4) found the prompt
    # classifier blind to 98% of labelled ripwire moments — most tasks never say what they need in the
    # words `--help-task` reads. The agent's first `Bash`/`Read` call is a cleaner signal: `grep -rn NAME
    # src/` already names the verb (`--grep`) and its argument; a `Read` of a source file already names
    # the file `--for` would take, or the symbol `--expand` would take once it resolves. The retired nudge
    # (hooks/ripwire-nudge.sh, §RETIRED) fired on the same moment with generic text and no argument; this
    # arm pastes the exact command instead — the same difference the prompt router made over grep, applied
    # one moment earlier.
    #
    # THE ARM IS THE METER'S ARM, NOT A SECOND COIN FLIP. `resolve_arm` below reimplements the exact rule
    # meter_init() (hooks/ripwire-nudge.sh) and hooks/ripwire-claude-route.sh's own `resolve_arm` apply —
    # env over `meter.conf` over the `auto` default (issue #381; it was `treatment`), `auto` selecting the same stable session-id hash
    # — so a session lands on the same side across all three instruments. A control session runs the same
    # classification and writes the identical row; it injects nothing. Reimplemented rather than sourced:
    # sourcing either sibling script would run its own top-level PreToolUse/UserPromptSubmit body.
    #
    # ROUTER=TOOLCALL, NEVER POOLED WITH router=prompt. Both routers append to the SAME
    # ~/.ripwire/routing.jsonl (one file, one contamination surface, matching the substitution meter's own
    # "one global log" posture) but every row here carries `router:"toolcall"`; hooks/ripwire-claude-route.sh
    # was updated in the same round to carry `router:"prompt"` on its own rows so the two populations are
    # distinguishable by a single field rather than by absence-of-field. bench/routing_ab_report.py reads
    # `router` and reports each router SEPARATELY — the pre-registered n>=40-per-arm floor applies per
    # router per arm, and a reader that pooled them would be comparing two different questions.
    #
    # WHAT THIS ROW DOES NOT DO: adoption tracking. Unlike the prompt router's `RouteObservation` (which
    # needs hooks/ripwire-nudge.sh's PreToolUse path to see the NEXT tool call after a prompt), this hook
    # sees every tool call already — but a live adoption-within-two loop for THIS arm was deliberately left
    # out of THIS build (see LANE_REPORT.md): the substitution meter (hooks/ripwire-nudge.sh's §METER,
    # ~/.ripwire/substitution.jsonl) already logs every observed tool call in session+seq order, so the
    # control arm's counterfactual ("how often does a routable event get followed by a ripwire call
    # anyway", measured at 0.086 from the local meter) is computable by correlating THIS log's decision
    # rows against THAT log after the fact, without this hook needing its own pending-file chain. Building
    # that correlator is follow-up work, named as such in docs/SUBSTITUTION_METER.md and LANE_REPORT.md.
    #
    # ONE ROW PER DECISION, NEVER RAW TEXT. A row is written for every ROUTABLE EVENT — a Bash command that
    # looks like a recursive grep/rg, a `Grep` tool call, or a `Read` tool call — in BOTH arms, with
    # `status` (`recommend`/`abstain`), `reason` (why an abstain happened), and `recommended` (the bare
    # verb, e.g. `--grep`, never the argument). Anything else (Edit/Write/Task/Glob/mcp__*, or a Bash
    # command that is not a recursive grep — `cat`, `sed`, `ps`, `tail -f`, a build/test invocation, a
    # plain `git` command) is not a routable event at all and exits before any row is written or any
    # subprocess is spawned, mirroring hooks/ripwire-nudge.sh's own fast-bail discipline.
    #
    # NOTIFICATION-SHAPED INPUT NEVER ROUTES. A command, pattern, or path carrying the literal substrings
    # `[SYSTEM NOTIFICATION` or `<task-notification>` (the harness's own wake-up markers, per
    # bench/mine_traces.py's §2.1 origin-kind filter) is treated as instrumentation noise, not a real
    # retrieval need, and abstains with reason="notification" before any other classification runs.
    #
    # PER-SESSION CAP OF THREE RECOMMENDATIONS, same reasoning as the retired nudge's own design note: an
    # uncapped hint on every grep is noise. A marker file under $TMPDIR, keyed by the raw session id,
    # counts recommendations already made (one byte appended per recommend, same atomic-append trick
    # hooks/ripwire-nudge.sh's meter_log uses for `seq`). The 4th-and-later routable event in a session
    # still gets a row — `status="abstain" reason="cap"` — but injects nothing, in either arm.
    #
    # THE RESOLVED_SYMBOLS GUARD (unchanged from the prompt router / --help-task's own rule): a `Read` of a
    # source file is recommended as `--expand=<symbol>` only when a guessed symbol name actually resolves
    # on the root (`ripwire <root> --expand=<candidate>` exits 0). It almost always will not — most files
    # are not named exactly after the one symbol inside them — and the deliberate, honest fallback is
    # `--for="<file>"`, which never needs a resolved symbol and is always a sound thing to ask for a known
    # file. Guessing a symbol that does not resolve and recommending it anyway is exactly the harmful shape
    # this arm's own gate (test/toolcallroutecheck.sh) is written to catch.
    #
    # PROMPT-INJECTION / PRIVACY POSTURE, same contract as hooks/ripwire
  • hooks/ripwire-codex-nudge.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Convert the shared advisory hook's Claude-compatible output into Codex's native hook shape.
    # Classification, deduplication, routing suggestions, and metering stay in ripwire-nudge.sh.
    set -u
    
    dir="$( cd "$( dirname "$0" )" && pwd )"
    # §METER v3: rows written through this wrapper are Codex's, and the shared hook's `agent` field
    # defaults to claude — name the runner unless the operator already did.
    export RIPWIRE_METER_AGENT="${RIPWIRE_METER_AGENT:-codex}"
    input="$( cat )" || exit 0
    out="$( printf '%s' "$input" | "$dir/ripwire-nudge.sh" "$@" )"
    rc=$?
    [ "$rc" -eq 0 ] || exit "$rc"
    [ "${1:-}" = "--session-start" ] || printf '%s' "$input" | "$dir/ripwire-codex-route.sh" --observe >/dev/null 2>&1 || true
    [ -n "$out" ] || exit 0
    
    if command -v jq >/dev/null 2>&1; then
        # Codex accepts additionalContext directly, but `permissionDecision: allow` is valid only when
        # accompanied by updatedInput. The hook is advisory, so remove that Claude compatibility field.
        printf '%s\n' "$out" | jq 'if .hookSpecificOutput.permissionDecision == "allow" and
            (.hookSpecificOutput.updatedInput | not) then del(.hookSpecificOutput.permissionDecision) else . end'
    else
        # Never block the triggering tool merely because the optional output adapter is unavailable.
        exit 0
    fi
    
  • hooks/ripwire-codex-route.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Codex UserPromptSubmit router: ask the deterministic --help-task classifier before the first tool
    # choice, inject one CLI recommendation only at high confidence, and instrument the decision without
    # retaining prompt text. The --observe arm is called by ripwire-codex-nudge.sh on PreToolUse and closes
    # the registered adoption-within-two loop. Advisory-only: any missing dependency/error degrades to silence.
    set -u
    
    command -v jq >/dev/null 2>&1 || exit 0
    input="$( cat )" || exit 0
    
    meter_home()
    {
        [ "${RIPWIRE_ROUTE_METER:-1}" != 0 ] || return 1
        meterHome="${RIPWIRE_HOME:-${HOME:+$HOME/.ripwire}}"
        [ -n "$meterHome" ] || return 1
        mkdir -p "$meterHome/routing-pending" 2>/dev/null || return 1
        routingLog="$meterHome/routing.jsonl"
        # A session that ends before two Ripwire calls leaves its pending file behind; expire the strays so
        # routing-pending/ never accumulates unboundedly. Best-effort, like everything else in the meter.
        find "$meterHome/routing-pending" -type f -name '*.json' -mtime +7 -delete 2>/dev/null || true
    }
    
    hash_text()
    {
        printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1
    }
    
    # ---- BEGIN MIRRORED BLOCK rw_is_ripwire_call (PR #215 review item 6) -------------------------------------
    # KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh, hooks/ripwire-codex-route.sh and hooks/ripwire-nudge.sh.
    # test/routehookcheck.sh extracts the three copies and diffs them, the kIngestParserVerMirror pattern: three
    # files answering one question must answer it in one text, or the meter and the hooks disagree about the very
    # same command line — which is exactly what happened, and it makes the adoption numbers unreadable.
    #
    # WHAT ROUND 1 REPLACED. A regex that looked for `ripwire` after a separator. It said NO to every WRAPPED
    # invocation an agent actually types — `time ./build/ripwire .`, `sudo ripwire`, `env RIPWIRE_BIN=x ripwire`,
    # `xargs ripwire`, `exec ripwire`, `nohup ripwire`, `if ripwire … ; then`, `{ ripwire … ; }` — and still said
    # YES to `git commit -m "fix; ripwire hook"`, where the word sits inside a quoted string and no ripwire runs.
    # Both errors corrupt the same measurement in opposite directions.
    #
    # WHAT ROUND 2 REPLACES (CodeRabbit, PR #215). Round 1 asked the SHELL to split the line — `set -- $1` with
    # globbing off — and then walked the words. Word splitting is not lexing: it never separates a control
    # operator from the word it is attached to. `true; ripwire .` split into `true;` and `ripwire`; `true;` was
    # read as an ordinary command word, so the `ripwire` behind it was no longer in command position and the call
    # was missed. Every `a; ripwire`, `a&&ripwire`, `a|ripwire`, `(ripwire .)` shape went the same way — and
    # those are the shapes an agent's one-liner is actually made of. Round 1 also got the quoted-string case
    # right for the WRONG reason (`-m "fix;` happened not to end a command), which is not a property to rest a
    # published ratio on.
    #
    # WHAT IT DOES NOW. It LEXES the line itself, one character at a time, and never expands, evaluates or
    # executes any part of it: backslash escapes, 'single' and "double" quotes, and the unquoted control
    # operators `;` `&` `|` `(` `)` and newline, each of which ends the current word AND puts the next word in
    # command position. An unquoted `#` starting a word ends the scan — the rest is a comment. `<` and `>` end a
    # word and consume the next one as a redirection target, leaving command position where it was. On top of
    # that sits the same command-position rule round 1 used: the wrapper words below do not consume the command,
    # `cd DIR`, `rtk proxy` and `VAR=value` prefixes are stepped over with their operand, and the word is
    # basename'd, so `./build/ripwire` and `/opt/rw/ripwire` count while `/opt/ripwire/bin/other` does not.
    # Quoted text can no longer reach command position by construction, so `git commit -m "fix; ripwire hook"`
    # and `grep -r 'ripwire;' src/` read as what they are: appearances that run nothing.
    #
    # KNOWN LIMIT, disclosed rather than papered over: a redirection written `2>&1` sends its `&` through the
    # control-operator branch, so the digit behind it is read as a command word. That can only ever cost a
    # MISSED call, in a line where `ripwire` sits in exactly that position, and never a false one.
    #
    # COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match
    # around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS
    # bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the
    # word as written holds no call, and that check ends the scan for nearly every command. It reads the raw text,
    # before quote removal, so a command word the shell assembles from quoted or escaped fragments (`'rip''wire'`,
    # `rip\wire`, `"rip""wire"`) reads as no call: a MISSED call, never a false one. A line longer than 1,024
    # characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024
    # costs under half a second at worst. test/routehookcheck.sh O10 holds the cost, O9 pins the assembled words.
    #
    # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`.
    rw_cmd_word()
    {
        # One completed word, offered to the command-position rule. Returns 0 only for a call.
        if [ "$rw_rtk" = 1 ]
        then
            rw_rtk=0
            if [ "$1" = "proxy" ]; then return 1; fi
        fi
        if [ "$rw_skip" -gt 0 ]
        then
            rw_skip=$(( rw_skip - 1 ))
            return 1
        fi
        if [ "$rw_at_cmd" != 1 ]
        then
            return 1
        fi
        case "$1" in
            if|while|until|do|then|else|elif|done|fi|esac|'!'|'{'|'}')  return 1 ;;
            *=*)                                                        return 1 ;;
            sudo|command|env|time|nice|nohup|exec|builtin|xargs)         return 1 ;;
            cd|pushd)                                        rw_skip=1; return 1 
  • hooks/ripwire-nudge.shGitHub
    Read the script
    #!/usr/bin/env bash
    # hooks/ripwire-nudge.sh — OPT-IN Claude Code PreToolUse + SessionStart hook.
    #
    # READ §RETIRED FIRST. As of 2026-09-02 the PreToolUse path SAYS NOTHING: a randomized A/B measured
    # both nudge tiers inert and the registered consequence was applied. What survives on that path is the
    # SUBSTITUTION METER (§METER) plus the eligibility bookkeeping that tells a later readout which calls
    # the retired advice would have landed on. The SessionStart primer still speaks, and is still the one
    # thing the control arm does not get. The design record below describes a mechanism that is GONE; it
    # is kept because a measured negative whose reasoning is deleted gets rebuilt by the next person.
    #
    # The original two-jobs description follows. Historically: two jobs in one script, the ADVISORY-ONLY
    # nudge (below) and the SUBSTITUTION METER (see the §METER block further down).
    #
    # Nudged an agent from raw grep/rg, from whole-file Read / candidate-Glob, and from raw `git diff`/
    # `git log`/`git show --stat`, toward the matching ripwire verb. Ships INACTIVE — only registered via
    # `skills/install.sh --hook`, never automatically (see the Phase B5.2 / R5 agent-context-science
    # design: "with grep available, the agent defaults to it... a PreToolUse hook is the high-leverage
    # lever" — passive skill-description triggering alone is measured ~30-50% reliable). Phase B9
    # (Part 3) extends the trigger set to the git-information moments where raw
    # output is weakest vs ripwire's structured answers: `git diff` → `--situ`/`--pr-context`, `git log` →
    # `--rank-by=churn`/`--map-diff`, `git show --stat` → `--map-diff`. Deliberately excluded: `git status`
    # and state-changing commands (add/commit/push/pull/checkout/branch) — nudging those is spam.
    #
    # 2026-08-10 (skill-orientation audit): Read and Glob added. They were previously excluded by the fast
    # bail, which left the LARGEST token sink in the loop — the whole-file read — as the one default this
    # hook could not see. A skill description only fires if the agent first RECOGNIZES a moment and then
    # spends a call to load the skill; `Read` needs neither. The read nudge is therefore the one that
    # matters most, and it is why the Read case is deliberately NOT deduped against the grep case: they
    # are different habits and each gets its own one-time nudge.
    #
    # The read nudge fires ONCE per session and deliberately does NOT try to name the symbol involved:
    # suggesting `--expand=<guess>` when the guess is wrong teaches an agent the tool is unreliable, which
    # costs more than the nudge gains. It names the verb and lets the agent supply the argument.
    #
    # 2026-08-29 (P4.2): the grep/rg base nudge no longer fires on a short single-literal pattern (the
    # case our own docs concede to `rg`) — only on a multi-pattern/OR-chain search, where the bundle
    # genuinely wins. See §CEDE, near where `category` is demoted, for the full rationale; the sweep
    # escalation below is untouched and still escalates on a same-class run of literal greps.
    #
    # Design posture (non-negotiable — do not "improve" this into a blocker):
    #   - NEVER blocks, denies, or rewrites the tool call. Always permissionDecision "allow" when it
    #     speaks at all; never "deny"/"ask"; never "updatedInput". Any internal failure (missing jq,
    #     malformed JSON, no git, no ripwire) degrades to silent allow — exit 0, no stdout.
    #   - Fires at MOST once per session per pattern (Grep, Bash-grep, git-diff, git-log, and git-show-stat
    #     are separate patterns), via a marker file under $TMPDIR keyed by session_id (falls back to PPID
    #     if the payload has none) — an agent sees each nudge once, not on every call.
    #   - Only fires when the target directory is inside a git repo AND a `ripwire` binary is on PATH.
    #   - Exits fast: the common case (any tool other than Grep/Bash, or a Bash command that matches none
    #     of recursive grep/rg / `git diff` / `git log` / `git show --stat`) bails after one or two greps
    #     over stdin — no subprocess spawned.
    #
    # Contract (verified against https://code.claude.com/docs/en/hooks, 2026-07-13): PreToolUse JSON
    # arrives on stdin with at least {session_id, cwd, tool_name, tool_input}. Advisory feedback that does
    # NOT block the call = exit 0 + this JSON on stdout:
    #   {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow",
    #    "additionalContext":"..."}}
    #
    # Usage (wired by the installer, not meant to be run by hand):
    #   printf '%s' "$PRETOOLUSE_JSON" | hooks/ripwire-nudge.sh
    set -u
    
    # ═══════════════════════════════════════════════════════════════════════════════════════════════════
    # §METER — THE SUBSTITUTION METER (Track B §S2, 2026-08-11)
    #
    # WHAT IT MEASURES, AND WHY THE UNIT IS A TOOL CALL. The question this project actually wants answered
    # is "does an agent with ripwire installed reach for it instead of grep/read?". The task-success
    # outcome eval cannot answer it: the power arithmetic puts the minimum detectable effect at 8.5-26pp
    # against a pilot that measured parity, so that instrument is dead no matter how long it runs. A
    # PreToolUse hook, on the other hand, sits at the exact decision point — the moment a default is
    # chosen — and every session produces hundreds of those. So the unit of observation is one TOOL CALL,
    # and the metric is a rate over calls: ripwire calls / (ripwire + native retrieval calls).
    #
    # ONE GLOBAL LOG. Rows append to $RIPWIRE_HOME/substitution.jsonl (default ~/.ripwire/), created
    # lazily, one row per line, each tagged with the repo it came from. The hook is registered per-user
    # but runs in whatever repo the session is in, so a single global file with a `tag`/`repo` field is
    # what makes both cross-repo and per-repo analysis possible from the same data. bench/
    # substitution_report.py reads it; docs/SUBSTITUTION_METER.md is the schema and the classifier's
    # rule table. One log means one contamination risk, so the §FIXTURE guard in meter_init() is part of
    # the design and not a test detail: a gate run must neve

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 withripwire

The ripgrep of AI context. A map before your agent reads the repo — and a check on what it writes. Ranked, deterministic call graph: what to touch, what it breaks, which tests to run.

Get the whole plugin
Stats
2,449
Stars
155
Forks
Active
Maintenance
C++
Language
Apache-2.0
License
1h ago
Last commit
2mo ago
Created
4h ago
Added

Repo: redhat-et/ripwire