Skip to content
Productivity
Hook

Hooks

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

From plugin
bedrock
10110 skills1 hook
Install
> /plugin marketplace add iurykrieger/claude-bedrock
> /plugin install bedrock@claude-bedrock

Ships with bedrock. Installing the plugin gets these hooks.

What fires, and when

Stop

  • python3 "${CLAUDE_PLUGIN_ROOT}/hooks/error_reporter.py"
Read hooks/hooks.json

In the plugin's words

How bedrock describes its own hook set.

Auto-report bedrock framework errors as GitHub issues

Where it lives

  • hooks/error_reporter.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Bedrock error reporter hook — auto-creates GitHub issues for framework errors.
    
    Stop hook entrypoint. Reads transcript via stdin JSON, scans the last turn
    for bedrock framework errors (technical + logical), opens deduplicated issues
    on iurykrieger/claude-bedrock via the gh CLI.
    
    Never raises, always exits 0. Failures log to ~/.claude-bedrock-cache/error-reporter.log.
    """
    import hashlib
    import json
    import os
    import platform
    import re
    import subprocess
    import sys
    import time
    from datetime import datetime, timezone
    from pathlib import Path
    from typing import Iterable
    
    
    # Keep window small for performance: only the most recent N lines matter,
    # since hook fires per turn and older lines are from prior turns.
    _TRANSCRIPT_TAIL_LINES = 200
    
    _BEDROCK_INVOCATION_RE = re.compile(r"/bedrock:(\w+)")
    
    _TRACEBACK_RE = re.compile(r"Traceback \(most recent call last\):", re.MULTILINE)
    _LAST_FRAME_RE = re.compile(r'File "([^"]+)", line (\d+), in (\w+)\n((?!\s*File ")[^\n]*\n)?([A-Z][\w\.]+(?:Error|Exception):.*)', re.MULTILINE)
    
    # Regex catalog. ID -> compiled regex. Keep small to avoid false positives.
    _LOGICAL_ERROR_CATALOG = {
        "graphify_invalid": re.compile(r"graphify.{0,40}(returned|gave|produced).{0,20}invalid", re.IGNORECASE),
        "vault_corrupt": re.compile(r"vault\.json.{0,30}corrupt", re.IGNORECASE),
        "skill_failure": re.compile(r"bedrock\s+\w+\s+(skill\s+)?failed", re.IGNORECASE),
        "entity_unwritable": re.compile(r"failed\s+to\s+(write|persist)\s+entity", re.IGNORECASE),
        "sync_unauthorized": re.compile(r"(sync.{0,30}unauthorized|auth(?:entication)?\s+failed.{0,30}sync)", re.IGNORECASE),
    }
    
    # Redaction regexes — order matters in redact(). See function docstring.
    _HOME_PATH_RE = re.compile(r"(?:/Users/[^/\s]+|/home/[^/\s]+|/root)(?:/[^/\s]+)*?(?=/\.claude/plugins/[^/\s]+/[^/\s]+)")
    _GENERIC_HOME_PATH_RE = re.compile(r"(?:/Users/[^/\s]+|/home/[^/\s]+|/root)")
    _PLUGIN_PREFIX_RE = re.compile(r"\.claude/plugins/[^/\s]+/[^/\s]+/")
    _UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE)
    _URL_RE = re.compile(r"https?://[^\s'\"<>)]+", re.IGNORECASE)
    _ISO_TIMESTAMP_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:?\d{2})?")
    _VAULT_ENTITY_FILE_RE = re.compile(r"\b(?:people|teams|actors|concepts|topics|discussions|projects|fleeting)/[\w-]+\.md\b")
    _EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
    _CC_LIKE_RE = re.compile(r"\b\d[\d -]{11,17}\d\b")
    _API_KEY_RE = re.compile(r"\b(?:sk_(?:live|test)|ghp|ghs|gho|ghu|ghr|xox[abps]|AKIA|ASIA|GITHUB_TOKEN)[\w_-]{10,}\b", re.IGNORECASE)
    _BARE_WIKILINK_RE = re.compile(r"\[\[[\w/-]+\]\]")
    
    
    def _read_transcript_tail(transcript_path: Path) -> str:
        """Read up to the last _TRANSCRIPT_TAIL_LINES of a JSONL transcript.
    
        Returns empty string if the file does not exist or is unreadable.
        """
        try:
            with open(transcript_path, "r", encoding="utf-8", errors="replace") as f:
                lines = f.readlines()
        except (FileNotFoundError, OSError):
            return ""
        return "".join(lines[-_TRANSCRIPT_TAIL_LINES:])
    
    
    def contains_bedrock_invocation(transcript_path: Path) -> bool:
        """Fast gate: returns True if '/bedrock:' appears in the recent transcript tail.
    
        Intentionally a substring check, not JSON parsing — optimized for the 99% case
        where the answer is no. False positives (e.g., the substring quoted in an
        assistant message) only cost an extra slow-path traversal in the next stage;
        false negatives would mean lost error reports, which is the worse failure mode.
        """
        tail = _read_transcript_tail(Path(transcript_path))
        return "/bedrock:" in tail
    
    
    def is_reporting_enabled(start_dir: Path) -> bool:
        """Walk up from start_dir looking for .bedrock/config.json.
    
        Returns True (default) if no config found, config malformed, or field missing.
        Returns False only if config explicitly sets error_reporting to the JSON `false`
        boolean — null, "false" strings, 0, etc. all default-on. Opt-out must be
        well-formed and intentional.
        """
        current = Path(start_dir).resolve()
        for candidate in [current, *current.parents]:
            cfg_path = candidate / ".bedrock" / "config.json"
            if cfg_path.is_file():
                try:
                    with open(cfg_path, "r", encoding="utf-8") as f:
                        cfg = json.load(f)
                except (json.JSONDecodeError, OSError):
                    return True  # default-on if config unreadable
                val = cfg.get("error_reporting", True)
                return val if isinstance(val, bool) else True
        return True
    
    
    def _iter_transcript_lines(transcript_path: Path) -> Iterable[dict]:
        """Yield parsed JSON objects from each non-empty JSONL line. Skips malformed lines."""
        try:
            with open(transcript_path, "r", encoding="utf-8", errors="replace") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        yield json.loads(line)
                    except json.JSONDecodeError:
                        continue
        except (FileNotFoundError, OSError):
            return
    
    
    def _iter_last_turn_lines(transcript_path: Path) -> list[dict]:
        """Return parsed JSON entries from the last turn.
    
        A turn boundary is defined by a role=user entry containing a type=text content
        block (i.e., a user-typed message, not a tool_result). Returns the last such
        entry and everything after it. If no boundary is found, returns all entries.
        """
        entries = list(_iter_transcript_lines(transcript_path))
        if not entries:
            return []
    
        # Walk backwards to find the last user-typed message
        last_user_text_idx = None
        for idx in range(len(entries) - 1, -1, -1):
            entry = entries[idx]
            if entry.get("role") != "user":
                continue
            for block in entry.get("message", {}).get("content", []) or []:
                if isinstance(bl

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 withbedrock

Second Brain automation for Obsidian vaults — entity management, ingestion, compression, and sync via Claude Code skills

Get the whole plugin
Stats
101
Stars
9
Forks
Maintained
Maintenance
HTML
Language
MIT
License
4mo ago
Last commit
5mo ago
Created

Repo: iurykrieger/claude-bedrock