SkillsHooksPromptsAgentsPersonasModelsPoliciesToolsTemplatesBundlesCategoriesStart here
← Hooks
Hk hooksecurityadvisorystable

1Password & Secret Redactor

PreToolUse hook that redacts 1Password references and secret-shaped values from Claude Code tool-use payloads before they execute. Unlike secret-block which denies, this hook redacts in-place and always exits 0 — it never blocks the tool call. Patterns: GitHub tokens (gho_/ghp_/ghu_/ghs_/ghr_), Bearer tokens, op:// references, OpenAI sk- keys, PEM blocks. Writes a violation record on detection.

id hook/op-redactv1.0.0by convergent-systems-key
Event
PreToolUse
Trigger
always
Language
python
Side effects
  • redacts secrets in-place in tool payload
  • writes violation record to ~/.ai/audit/violations/<UTC>-secret-detected.md
  • outputs cleaned JSON to stdout
Platforms
linuxmacoswindows
Notes
Logic is cross-platform. Wiring: use 'ai hooks run op-redact' in settings.json — the ai binary discovers Python on each OS. Pure Python regex; no OS-specific calls. Works on all platforms.

Script · op-redact.py

#!/usr/bin/env python3
"""hooks/op-redact.py — PreToolUse hook that redacts 1Password and other
secret-shaped values from Claude Code tool-use payloads.

Unlike secret-block.py (which DENIES the tool call), this hook:
  - Redacts matching strings in-place across all string fields.
  - Writes a violation record to $AI_ROOT/audit/violations/<UTC>-secret-detected.md.
  - Outputs the cleaned JSON to stdout.
  - ALWAYS exits 0 — it never blocks the tool call.

Per Common.md §4 (non-overridable: secrets must not appear in artifacts)
and SPEC.md §10.1.

Redaction patterns (inline — no dependency on patterns.json):
  gho_ / ghp_ / ghu_ / ghs_ / ghr_  + 36+ chars → [REDACTED:github-token]
  github_pat_ + 60+ chars             → [REDACTED:github-token]
  Bearer  + 20+ chars                 → [REDACTED:bearer-token]
  op://                               → [REDACTED:op-ref]
  sk- + 40+ chars                     → [REDACTED:openai-key]
  -----BEGIN                          → [REDACTED:pem-block]

Input contract (Claude Code PreToolUse event):
  - Full tool-use payload arrives on stdin as JSON.
  - Exit 0 always (redact + log, never block).
  - stdout: cleaned JSON.
  - stderr: human-readable diagnostic lines.

Self-check:
  --self-check  Verifies regex compilation; exits 0 on success.
"""
from __future__ import annotations

import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

# ---------------------------------------------------------------------------
# Inline redaction patterns — no dependency on patterns.json.
# Each tuple: (compiled_regex, replacement_string, kind_label)
# ---------------------------------------------------------------------------

_RAW_PATTERNS: list[tuple[str, str, str]] = [
    # GitHub classic tokens
    (r"(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}", "[REDACTED:github-token]", "github-token"),
    # GitHub fine-grained PAT
    (r"github_pat_[A-Za-z0-9_]{60,}", "[REDACTED:github-token]", "github-token"),
    # Bearer tokens (20+ chars after 'Bearer ')
    (r"Bearer [A-Za-z0-9._\-]{20,}", "[REDACTED:bearer-token]", "bearer-token"),
    # op:// references — redact the entire op:// URI
    (r"op://[^\s\"']+", "[REDACTED:op-ref]", "op-ref"),
    # OpenAI-style keys: sk- followed by 40+ chars (excludes sk-ant- Anthropic keys
    # which have their own label, but this is belt-and-suspenders)
    (r"sk-[A-Za-z0-9_\-]{40,}", "[REDACTED:openai-key]", "openai-key"),
    # PEM block headers
    (r"-----BEGIN[^\n\r]*", "[REDACTED:pem-block]", "pem-block"),
]

_PATTERNS: list[tuple[re.Pattern, str, str]] = [
    (re.compile(raw), repl, kind)
    for raw, repl, kind in _RAW_PATTERNS
]


def redact_string(value: str) -> tuple[str, list[str]]:
    """Apply all patterns to value. Returns (redacted_value, list_of_kind_hits)."""
    hits: list[str] = []
    out = value
    for pattern, replacement, kind in _PATTERNS:
        new, n = pattern.subn(replacement, out)
        if n > 0:
            hits.append(kind)
            out = new
    return out, hits


def redact_recursive(obj: object) -> tuple[object, list[str]]:
    """Walk obj depth-first and redact all string leaves.
    Returns (cleaned_obj, all_hit_kinds)."""
    all_hits: list[str] = []
    if isinstance(obj, str):
        cleaned, hits = redact_string(obj)
        return cleaned, hits
    if isinstance(obj, dict):
        out = {}
        for k, v in obj.items():
            cleaned_v, hits = redact_recursive(v)
            out[k] = cleaned_v
            all_hits.extend(hits)
        return out, all_hits
    if isinstance(obj, list):
        out_list = []
        for item in obj:
            cleaned_item, hits = redact_recursive(item)
            out_list.append(cleaned_item)
            all_hits.extend(hits)
        return out_list, all_hits
    # int, float, bool, None — pass through
    return obj, []


def ai_root() -> Path:
    """Return the canonical ~/.ai/ root, honoring $AI_ROOT."""
    env = os.environ.get("AI_ROOT", "")
    if env:
        return Path(env)
    return Path.home() / ".ai"


def write_violation(kinds: list[str], payload_summary: str) -> None:
    """Write a violation record to $AI_ROOT/audit/violations/<UTC>-secret-detected.md."""
    now = datetime.now(tz=timezone.utc)
    ts = now.strftime("%Y-%m-%dT%H%M%SZ")
    violations_dir = ai_root() / "audit" / "violations"
    try:
        violations_dir.mkdir(parents=True, exist_ok=True)
        path = violations_dir / f"{ts}-secret-detected.md"
        unique_kinds = sorted(set(kinds))
        body = f"""# Violation — {ts}

- **File / Rule violated:** Common.md/§4 — No Secrets In Artifacts
- **What happened:** op-redact.py PreToolUse hook detected {len(kinds)} secret-like match(es) in a Claude Code tool-use payload and redacted them before the payload was processed. Pattern kinds: {', '.join(unique_kinds)}.
- **How noticed:** tool-flagged (op-redact.py)
- **Remediation:** Values were replaced with [REDACTED:<kind>] in the cleaned payload written to stdout. The original payload was not passed downstream.
- **Payload summary (redacted):** {payload_summary[:200]}
"""
        path.write_text(body, encoding="utf-8")
    except OSError as exc:
        # Logging failures must not cause the hook to block.
        print(f"[ai/op-redact] WARNING: could not write violation file: {exc}", file=sys.stderr)


def hook_name() -> str:
    return "op-redact"


def log(*parts) -> None:
    print(f"[ai/{hook_name()}]", *parts, file=sys.stderr, flush=True)


def self_check_ok() -> int:
    """Compile all patterns; exit 0 if OK."""
    try:
        for raw, _, _ in _RAW_PATTERNS:
            re.compile(raw)
    except re.error as exc:
        log(f"self-check FAIL: regex compile error: {exc}")
        return 1
    log("self-check OK")
    return 0


def main(argv: list[str]) -> int:
    if "--self-check" in argv:
        return self_check_ok()

    raw = sys.stdin.read()
    if not raw.strip():
        # Empty payload — nothing to redact; pass through empty.
        sys.stdout.write("")
        return 0

    # Parse payload. Fall back to wrapping raw in a dict so we can still
    # scan it for patterns.
    try:
        payload = json.loads(raw)
        parse_ok = True
    except json.JSONDecodeError:
        payload = {"_raw": raw}
        parse_ok = False

    cleaned, hits = redact_recursive(payload)

    if hits:
        log(f"{len(hits)} secret-like match(es) redacted: {sorted(set(hits))}")
        log("Per Common.md §1.P4 (no secrets in artifacts; non-overridable).")
        # Summarize the cleaned output (already redacted) for the violation record.
        summary = json.dumps(cleaned)[:200]
        write_violation(hits, summary)

    # Always output the cleaned payload as JSON so the tool call proceeds.
    if parse_ok:
        sys.stdout.write(json.dumps(cleaned))
    else:
        # We were given unparseable JSON; output what we managed to clean.
        sys.stdout.write(cleaned.get("_raw", raw) if isinstance(cleaned, dict) else raw)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
securitysecretsredaction1passwordgovernanceclaude-code
Author convergent-systems-key. Catalog data license CC-BY-4.0.