Skip to content
Research
Hook

Hooks

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

From plugin
research-council
1072 commands1 hook
Install
> /plugin marketplace add hamelsmu/research-council
> /plugin install research-council@research-council

Ships with research-council. Installing the plugin gets these hooks.

What fires, and when

Stop

  • ${CLAUDE_PLUGIN_ROOT}/hooks/orchestrator-stop-hook.sh
Read hooks/hooks.json

In the plugin's words

How research-council describes its own hook set.

Deep Research Council: orchestrates parallel research across Claude and Codex

Where it lives

  • hooks/orchestrator-stop-hook.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Deep Research Council — Orchestrator Stop Hook
    #
    # Phase state machine:
    #   research   → run 2 parallel research agents → refinement
    #   refinement → run 2 parallel refinement agents → synthesis
    #   synthesis  → check for final report → allow exit
    #
    # Fail-open: on any unexpected error, allow exit (never trap the user).
    
    LOG_FILE=".claude/deep-research.log"
    
    log() {
      mkdir -p "$(dirname "$LOG_FILE")"
      echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] orchestrator: $*" >> "$LOG_FILE"
    }
    
    # Portable sed -i (macOS vs GNU)
    sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "$@"; else sed -i "$@"; fi; }
    
    # Escape a string for safe use in sed replacement patterns.
    # Must escape \ first (to avoid double-escaping), then & and /.
    sed_escape() { printf '%s\n' "$1" | sed -e 's/[\\]/\\&/g' -e 's/[&/]/\\&/g'; }
    
    # Guard: jq is required by this hook
    command -v jq &>/dev/null || { log "ERROR: jq not found in PATH"; exit 0; }
    
    # On any error, allow exit
    trap 'log "ERROR: hook exited via ERR trap (line $LINENO)"; rm -f ".claude/deep-research.lock"; printf "{\"decision\":\"approve\"}\n"; exit 0' ERR
    
    # Consume stdin
    HOOK_INPUT=$(cat)
    
    STATE_FILE=".claude/deep-research.local.md"
    LOCK_FILE=".claude/deep-research.lock"
    
    # No active session → allow exit
    if [ ! -f "$STATE_FILE" ]; then
      exit 0
    fi
    
    # ── Parse state file fields ──────────────────────────────────────────────
    parse_field() {
      sed -n "s/^${1}: *//p" "$STATE_FILE" | head -1
    }
    
    ACTIVE=$(parse_field "active")
    PHASE=$(parse_field "phase")
    RESEARCH_ID=$(parse_field "research_id")
    TEST_MODE=$(parse_field "test_mode")
    MAX_ITERS=$(parse_field "max_iterations")
    CLAUDE_MODEL=$(parse_field "claude_model")
    CODEX_MODEL=$(parse_field "codex_model")
    CODEX_REASONING=$(parse_field "codex_reasoning")
    
    # Not active → clean up
    if [ "$ACTIVE" != "true" ]; then
      rm -f "$STATE_FILE"
      exit 0
    fi
    
    # Validate research_id format
    if ! echo "$RESEARCH_ID" | grep -qE '^[0-9]{8}-[0-9]{6}-[0-9a-f]{6}$'; then
      log "ERROR: invalid research_id format: $RESEARCH_ID"
      rm -f "$STATE_FILE"
      exit 0
    fi
    
    # ── Lock helpers (defined early — used by session-ID check and phase execution) ──
    # Lock file stores "PID:EPOCH" to defend against PID-reuse false positives.
    # A lock older than 2 hours is treated as stale regardless of PID liveness.
    LOCK_MAX_AGE=7200  # 2 hours in seconds
    
    is_lock_alive() {
      [ -f "$LOCK_FILE" ] || return 1
      local lock_content pid lock_epoch now age
      lock_content=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
      pid="${lock_content%%:*}"
      lock_epoch="${lock_content#*:}"
      [ -z "$pid" ] && return 1
      # If lock has no epoch (legacy format), treat as stale
      [ "$lock_epoch" = "$pid" ] && return 1
      # Validate numeric format to avoid arithmetic errors on malformed content
      [[ "$pid" =~ ^[0-9]+$ ]] || return 1
      [[ "$lock_epoch" =~ ^[0-9]+$ ]] || return 1
      # Check age — stale locks are dead regardless of PID
      now=$(date +%s)
      age=$(( now - lock_epoch ))
      if [ "$age" -gt "$LOCK_MAX_AGE" ]; then
        log "WARN: lock is ${age}s old (max ${LOCK_MAX_AGE}s), treating as stale despite PID=$pid"
        return 1
      fi
      # PID must still be alive
      kill -0 "$pid" 2>/dev/null || return 1
      return 0
    }
    
    # ── Session ID check: skip if different session ──────────────────────────
    CURRENT_SESSION=$(echo "$HOOK_INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
    RECORDED_SESSION=$(parse_field "session_id")
    
    # First run: stamp the session ID into the state file
    if [ -z "$RECORDED_SESSION" ] && [ -n "$CURRENT_SESSION" ]; then
      sedi "s/^session_id:$/session_id: $(sed_escape "$CURRENT_SESSION")/" "$STATE_FILE"
      RECORDED_SESSION="$CURRENT_SESSION"
      log "Stamped session_id: ${CURRENT_SESSION}"
    fi
    
    # Different session — check if we can adopt the orphaned research
    if [ -n "$RECORDED_SESSION" ] && [ -n "$CURRENT_SESSION" ] && [ "$CURRENT_SESSION" != "$RECORDED_SESSION" ]; then
      # If a lock is held by a live, recent process, another session is actively running phases
      if is_lock_alive; then
        log "Skipping: different session and lock is active (current=$CURRENT_SESSION, recorded=$RECORDED_SESSION)"
        exit 0
      fi
      # No active lock — adopt the research into this session
      log "Adopting orphaned research into new session (old=$RECORDED_SESSION, new=$CURRENT_SESSION)"
      sedi "s/^session_id: .*$/session_id: $(sed_escape "$CURRENT_SESSION")/" "$STATE_FILE"
      RECORDED_SESSION="$CURRENT_SESSION"
    fi
    
    # ── Staleness check: auto-clean after 5 hours ────────────────────────────
    STARTED_AT=$(parse_field "started_at")
    if [ -n "$STARTED_AT" ]; then
      if [[ "$OSTYPE" == "darwin"* ]]; then
        STARTED_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || echo 0)
      else
        STARTED_EPOCH=$(date -d "$STARTED_AT" +%s 2>/dev/null || echo 0)
      fi
      NOW_EPOCH=$(date +%s)
      AGE=$(( NOW_EPOCH - STARTED_EPOCH ))
      if [ "$AGE" -gt 18000 ]; then  # 5 hours = 18000 seconds
        log "WARN: stale state file (${AGE}s old, started_at=$STARTED_AT), cleaning up"
        rm -f "$STATE_FILE"
        exit 0
      fi
    fi
    
    # Extract topic (everything after the second --- line in YAML front matter).
    # Only the first two --- lines are treated as delimiters; any --- in the
    # topic body (e.g. user researching YAML syntax) is passed through.
    TOPIC=$(awk '/^---$/ && count<2 {count++; next} count>=2' "$STATE_FILE" | sed '/^$/d')
    
    if [ -z "$TOPIC" ]; then
      log "ERROR: no topic found in state file"
      rm -f "$STATE_FILE"
      exit 0
    fi
    
    WORKSPACE="research/${RESEARCH_ID}"
    SCRIPT_DIR="$(cd "$(dirname "$0")/../scripts" && pwd)"
    PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
    
    # ── Lock acquire/release (is_lock_alive defined above) ────────────────
    acquire_lock() {
      if [ -f "$LOCK_FILE" ]; then
        if is_lock_alive; then
          local lock_content
          lock_content=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
          log "SKIP: another hook is running phases (lock=$lock_content)"
          jq -n '{decision:"block", reason:"Research agents are still running. Please wait."}'
          exit 0
        else
          log "WARN: removi

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 withresearch-council

A Claude Code plugin that orchestrates deep research across two coding agents — Claude Code and Codex CLI (OpenAI) — running in parallel with cross-pollination refinement and final synthesis.

Get the whole plugin
Stats
107
Stars
15
Forks
Quiet
Maintenance
Shell
Language
8mo ago
Last commit
8mo ago
Created
14h ago
Added

Repo: hamelsmu/research-council