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.
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"
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

