#!/bin/bash
# PostToolUse hook (matcher: Bash) — opt-in bash-output compression.
#
# Long agentic sessions degrade as context fills with noisy tool output:
# 200-line `git status` walls, npm/yarn install output, find/ls dumps. Tool
# results are the bulk of agentic context. This hook compresses only those
# known-noisy STDOUT shapes — head + tail + a visible marker — so the model
# reclaims context without losing signal.
#
# Platform contract (verified empirically 2026-06-07 against Claude Code
# v2.1.168 — see act:48a05cb7 spike): a PostToolUse hook rewrites the output
# the model sees by emitting
#   {"hookSpecificOutput":{"hookEventName":"PostToolUse",
#     "updatedToolOutput":{...}}}
# For the Bash tool, `updatedToolOutput` MUST be a STRUCTURED OBJECT matching
# the tool_response shape ({stdout, stderr, interrupted, isImage,
# noOutputExpected}). A plain string is silently IGNORED for Bash. Mirror
# every original field; rewrite only stdout.
#
# Safety invariants (do not relax — see maintainability rule "no silent
# failures"):
#   - In-memory only. The payload (which may contain secrets/env dumps) is
#     passed to python3 via an env var, never written to disk, temp, or logs.
#   - stderr is passed through VERBATIM. Errors are never compressed.
#   - Error/warning lines that fall in the elided region are preserved
#     verbatim inside the compressed block — a compressed-away error is a
#     silent failure.
#   - Every rewrite carries a visible marker:
#       [compressed: N lines -> M - rerun command for raw]
#   - Fail-OPEN: emits the rewrite JSON ONLY when it actually compresses;
#     any missing dependency, parse error, or internal failure => bare
#     exit 0 (original output passes through untouched).
#
# Opt-in only (off by default). Installed via the `bash-compress` module and
# wired by mergeBashCompressHooks() in lib/settings-merge.js.

PAYLOAD=$(cat)

# Heavy lifting in python3 (the repo idiom for JSON-parsing hooks). The
# payload arrives via env var so the heredoc can be the python SCRIPT on
# stdin. If python3 is absent or anything throws, OUT is empty / status is
# nonzero and we pass through.
OUT=$(CC_HOOK_PAYLOAD="$PAYLOAD" python3 <<'PYEOF' 2>/dev/null
import os, json, re, sys

raw = os.environ.get("CC_HOOK_PAYLOAD", "")
if not raw:
    sys.exit(0)
try:
    data = json.loads(raw)
except Exception:
    sys.exit(0)  # fail-open: malformed/truncated payload -> passthrough

resp = data.get("tool_response")
if not isinstance(resp, dict):
    sys.exit(0)
stdout = resp.get("stdout")
if not isinstance(stdout, str) or not stdout:
    sys.exit(0)

cmd = ""
ti = data.get("tool_input")
if isinstance(ti, dict):
    cmd = ti.get("command", "") or ""

# Very large => pass through (don't parse). Binary => pass through.
if len(stdout) > 5_000_000 or "\x00" in stdout:
    sys.exit(0)

# Compress only known-noisy command shapes. git diff is deliberately
# EXCLUDED: its content is signal the model reasons about, not a wall.
NOISY = re.compile(
    r"\b("
    r"git\s+status"
    r"|npm\s+(i|install|ci|ls)"
    r"|yarn(\s+install)?"
    r"|pnpm\s+(i|install)"
    r"|find\s"
    r"|ls\s"
    r"|pip3?\s+install"
    r"|brew\s+(install|update|upgrade)"
    r")\b"
)
if not NOISY.search(cmd):
    sys.exit(0)

lines = stdout.split("\n")
n = len(lines)

HEAD = 12
TAIL = 12
MIN_LINES = 40
MIN_SAVINGS = 10
if n <= MIN_LINES:
    sys.exit(0)

# Preserve error/warning lines verbatim even when they fall in the elided
# middle. Inclusive on purpose — never DROP a diagnostic line.
ERR = re.compile(
    r"(?i)(error|fatal|denied|permission|traceback|exception|panic"
    r"|segfault|cannot|could not|\bE[0-9]{2,}\b|warning)"
)
head = lines[:HEAD]
tail = lines[-TAIL:]
middle = lines[HEAD : n - TAIL]
preserved = [ln for ln in middle if ERR.search(ln)]

preserved_block = []
if preserved:
    preserved_block.append(
        "  ... %d preserved error/warning line(s) from elided region:"
        % len(preserved)
    )
    preserved_block.extend("  " + p for p in preserved)

m = len(head) + 1 + len(preserved_block) + len(tail)
if n - m < MIN_SAVINGS:
    sys.exit(0)  # not worth it -> passthrough

marker = "[compressed: %d lines -> %d - rerun command for raw]" % (n, m)
new_stdout = "\n".join(head + [marker] + preserved_block + tail)

# Mirror the tool_response shape; rewrite ONLY stdout. stderr and all other
# fields (interrupted, isImage, noOutputExpected, ...) pass through verbatim.
new_resp = dict(resp)
new_resp["stdout"] = new_stdout

print(
    json.dumps(
        {
            "hookSpecificOutput": {
                "hookEventName": "PostToolUse",
                "updatedToolOutput": new_resp,
            }
        }
    )
)
PYEOF
)
STATUS=$?

# Emit ONLY on a clean compression. Empty output or nonzero status => the
# original tool output passes through untouched (fail-open).
if [ "$STATUS" -eq 0 ] && [ -n "$OUT" ]; then
  printf '%s\n' "$OUT"
fi
exit 0
