|
1 | 1 | #!/usr/bin/env bash |
2 | | -# st2 Claude StopFailure hook: surface infrastructure wedges through presence and, when declared, |
3 | | -# the agent's supervisor inbox. Fail-open; lifecycle reporting must never wedge the harness. |
| 2 | +# st2 Claude StopFailure hook: append one private machine-local JSONL record, then surface |
| 3 | +# infrastructure wedges through presence and, when declared, the agent's supervisor inbox. |
| 4 | +# Fail-open; lifecycle reporting must never wedge the harness. |
4 | 5 |
|
5 | 6 | set -u |
6 | 7 |
|
7 | 8 | identity="${ST_AGENT:-}" |
8 | 9 | root="${ST_ROOT:-${CATALOG:-}}" |
9 | 10 | supervisor="${ST_SUPERVISOR:-}" |
| 11 | +input="$(cat 2>/dev/null || true)" |
| 12 | +error_type="unknown" |
| 13 | + |
| 14 | +# One file per identity avoids cross-agent append contention. The complete provider payload is |
| 15 | +# useful for diagnosis, but sensitive keys and token-shaped strings are redacted before the line |
| 16 | +# reaches disk. Oversized sanitized payloads keep a bounded preview. Invalid JSON is never copied. |
| 17 | +if command -v jq >/dev/null 2>&1; then |
| 18 | + timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || printf unknown)" |
| 19 | + record_identity="${identity:-unknown}" |
| 20 | + record="$( |
| 21 | + printf '%s' "$input" | jq -cs \ |
| 22 | + --arg timestamp "$timestamp" \ |
| 23 | + --arg identity "$record_identity" ' |
| 24 | + def sensitive_key: |
| 25 | + ascii_downcase |
| 26 | + | gsub("[^a-z0-9]"; "") |
| 27 | + | test("^(authorization|proxyauthorization|cookie|setcookie|password|passwd|secret|clientsecret|credential|apikey|privatekey|token|authtoken|bearertoken|accesstoken|refreshtoken|idtoken)$"); |
| 28 | + def redact_string: |
| 29 | + gsub("(?i)bearer[[:space:]]+[A-Za-z0-9._~+/=-]{8,}"; "Bearer [REDACTED]") |
| 30 | + | gsub("(?i)(access[_-]?token|refresh[_-]?token|api[_-]?key|client[_-]?secret|password|passwd|secret)[[:space:]]*[:=][[:space:]]*[^&[:space:],;]+"; "[REDACTED]") |
| 31 | + | gsub("(?i)(sk-[A-Za-z0-9_-]{12,}|github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16})"; "[REDACTED]") |
| 32 | + | gsub("[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{10,}"; "[REDACTED]"); |
| 33 | + def scrub: |
| 34 | + walk( |
| 35 | + if type == "object" then |
| 36 | + with_entries( |
| 37 | + if (.key | sensitive_key) then .value = "[REDACTED]" else . end |
| 38 | + ) |
| 39 | + elif type == "string" then |
| 40 | + redact_string |
| 41 | + else |
| 42 | + . |
| 43 | + end |
| 44 | + ); |
| 45 | + if length == 1 and (.[0] | type) == "object" then |
| 46 | + .[0] |
| 47 | + else |
| 48 | + error("expected one JSON object") |
| 49 | + end |
| 50 | + | . as $original |
| 51 | + | (($original.error_type // $original.error // "unknown") | tostring | redact_string) as $error_type |
| 52 | + | (scrub) as $payload |
| 53 | + | ($payload | tojson) as $encoded |
| 54 | + | { |
| 55 | + schema: 1, |
| 56 | + timestamp: $timestamp, |
| 57 | + event: "StopFailure", |
| 58 | + identity: $identity, |
| 59 | + error_type: $error_type, |
| 60 | + payload: ( |
| 61 | + if ($encoded | length) <= 16384 then |
| 62 | + $payload |
| 63 | + else |
| 64 | + {truncated: true, preview: $encoded[0:16384]} |
| 65 | + end |
| 66 | + ) |
| 67 | + } |
| 68 | + ' 2>/dev/null |
| 69 | + )" |
| 70 | + if [[ -z "$record" ]]; then |
| 71 | + record="$(jq -cn \ |
| 72 | + --arg timestamp "$timestamp" \ |
| 73 | + --arg identity "$record_identity" ' |
| 74 | + { |
| 75 | + schema: 1, |
| 76 | + timestamp: $timestamp, |
| 77 | + event: "StopFailure", |
| 78 | + identity: $identity, |
| 79 | + error_type: "unknown", |
| 80 | + payload: null, |
| 81 | + payload_error: "invalid_json" |
| 82 | + } |
| 83 | + ' 2>/dev/null || true)" |
| 84 | + fi |
| 85 | + error_type="$(printf '%s' "$record" | jq -r '.error_type // "unknown"' 2>/dev/null || printf unknown)" |
| 86 | + |
| 87 | + state_base="${XDG_STATE_HOME:-}" |
| 88 | + if [[ -z "$state_base" && -n "${HOME:-}" ]]; then |
| 89 | + state_base="${HOME}/.local/state" |
| 90 | + fi |
| 91 | + if [[ -n "$state_base" && -n "$record" ]]; then |
| 92 | + safe_identity="$(printf '%s' "$record_identity" | tr -c 'A-Za-z0-9._-' '_' 2>/dev/null || printf unknown)" |
| 93 | + record_dir="${state_base}/st2/hook-events/stop-failure" |
| 94 | + record_file="${record_dir}/${safe_identity}.jsonl" |
| 95 | + ( |
| 96 | + umask 077 |
| 97 | + mkdir -p "$record_dir" || exit 0 |
| 98 | + chmod 700 "$record_dir" || true |
| 99 | + printf '%s\n' "$record" >> "$record_file" || exit 0 |
| 100 | + chmod 600 "$record_file" || true |
| 101 | + ) 2>/dev/null || true |
| 102 | + fi |
| 103 | +fi |
| 104 | + |
10 | 105 | if [[ -z "$identity" || -z "$root" ]] || ! command -v st2 >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then |
11 | 106 | exit 0 |
12 | 107 | fi |
13 | 108 |
|
14 | | -input="$(cat)" |
15 | | -error_type="$(printf '%s' "$input" | jq -r '.error_type // "unknown"' 2>/dev/null || printf unknown)" |
16 | 109 | status="away" |
17 | 110 | notify="yes" |
18 | 111 | case "$error_type" in |
|
0 commit comments