Skip to content
AI & Agents
Hook

Hooks

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

From plugin
taskmaster
5311 skill2 hooks
Install
$ npx -y skills add blader/taskmaster --agent claude-code

Ships with taskmaster. Installing the plugin gets these hooks.

Where it lives

  • hooks/check-completion.shGitHub
    Read the script
    #!/usr/bin/env bash
    #
    # Stop hook: keep firing until the agent emits an explicit done signal.
    #
    # The stop is allowed only after the transcript contains:
    #   TASKMASTER_DONE::<session_id>
    #
    # Optional env vars:
    #   TASKMASTER_MAX          Max number of blocks before allowing stop (default: 0 = infinite)
    #
    set -u
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    # shellcheck disable=SC1091
    source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh"
    
    INPUT=$(cat)
    SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
    TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
    TRANSCRIPT="${TRANSCRIPT/#\~/$HOME}"
    if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then
      SESSION_ID="unknown-session"
    fi
    
    # --- skip subagents: they have very short transcripts ---
    if [ -f "$TRANSCRIPT" ]; then
      LINE_COUNT=$(wc -l < "$TRANSCRIPT" 2>/dev/null || echo "0")
      if [ "$LINE_COUNT" -lt 20 ]; then
        exit 0
      fi
    fi
    
    # --- counter ---
    COUNTER_DIR="${TMPDIR:-/tmp}/taskmaster"
    mkdir -p "$COUNTER_DIR"
    COUNTER_FILE="${COUNTER_DIR}/${SESSION_ID}"
    MAX=${TASKMASTER_MAX:-0}
    
    COUNT=0
    if [ -f "$COUNTER_FILE" ]; then
      COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
    fi
    
    transcript_has_done_signal() {
      local transcript_path="$1"
      local done_signal="$2"
    
      [ -f "$transcript_path" ] || return 1
    
      tail -400 "$transcript_path" 2>/dev/null \
        | jq -Rr '
            fromjson?
            | select(.type == "response_item" and .payload.type == "message" and .payload.role == "assistant")
            | .payload.content[]?
            | select(.type == "output_text")
            | .text // empty
          ' 2>/dev/null \
        | grep -Fq "$done_signal"
    }
    
    # --- done signal detection ---
    DONE_SIGNAL="TASKMASTER_DONE::${SESSION_ID}"
    HAS_DONE_SIGNAL=false
    HAS_RECENT_ERRORS=false
    
    # Primary: check last_assistant_message (most reliable — no transcript parsing needed)
    LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""')
    if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then
      HAS_DONE_SIGNAL=true
    fi
    
    
    # Fallback: check transcript file if last_assistant_message didn't match
    if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then
      if transcript_has_done_signal "$TRANSCRIPT" "$DONE_SIGNAL"; then
        HAS_DONE_SIGNAL=true
      fi
    
      if tail -40 "$TRANSCRIPT" 2>/dev/null | grep -qi '"is_error":\s*true'; then
        HAS_RECENT_ERRORS=true
      fi
    fi
    
    if [ "$HAS_DONE_SIGNAL" = true ]; then
      rm -f "$COUNTER_FILE"
      exit 0
    fi
    
    NEXT=$((COUNT + 1))
    echo "$NEXT" > "$COUNTER_FILE"
    
    # Optional escape hatch. Default is infinite (0) so hook keeps firing.
    if [ "$MAX" -gt 0 ] && [ "$NEXT" -ge "$MAX" ]; then
      rm -f "$COUNTER_FILE"
      exit 0
    fi
    
    if [ "$HAS_RECENT_ERRORS" = true ]; then
      PREAMBLE="Recent tool errors were detected. Resolve them before declaring done."
    else
      PREAMBLE="Stop is blocked until completion is explicitly confirmed."
    fi
    
    if [ "$MAX" -gt 0 ]; then
      LABEL="TASKMASTER (${NEXT}/${MAX})"
    else
      LABEL="TASKMASTER (${NEXT})"
    fi
    
    # --- reprompt ---
    SHARED_PROMPT="$(build_taskmaster_compliance_prompt "$DONE_SIGNAL")"
    REASON="${LABEL}: ${PREAMBLE}
    
    ${SHARED_PROMPT}"
    
    jq -n --arg reason "$REASON" '{ decision: "block", reason: $reason }'
    
  • hooks/inject-continue-codex.shGitHub
    Read the script
    #!/usr/bin/env bash
    #
    # Codex Taskmaster same-process injector (queue-emitter mode).
    # Watches a Codex session log and, on each incomplete task_complete/turn_complete,
    # writes a continuation prompt file into the expect bridge queue.
    #
    # Exit codes:
    #   0 = at least one done token observed
    #   2 = completion(s) observed but no done token
    #   3 = no completion events observed
    #   4 = invalid usage / prerequisites
    #
    set -euo pipefail
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    # shellcheck disable=SC1091
    source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh"
    
    usage() {
      cat <<'USAGE'
    Usage:
      inject-continue-codex.sh --log <session_log.jsonl> --emit-dir <dir> [--state-dir <dir>] [--follow] [--follow-latest-dir <dir>] [--latest-glob <glob>] [--quiet]
    
    Options:
      --log <path>      Path to CODEX_TUI_SESSION_LOG_PATH file.
      --emit-dir <dir>  Emit injection prompts as files in <dir>.
      --state-dir <dir> Persist follow-state in <dir> so a restarted injector resumes cleanly.
      --follow          Follow live updates until session_end.
      --follow-latest-dir <dir>  While following, switch to the newest matching log in <dir>.
      --latest-glob <glob>       Glob used under --follow-latest-dir. Default: taskmaster-session-*.jsonl
      --quiet           Suppress non-error output.
      -h, --help        Show help.
    USAGE
    }
    
    LOG_PATH="${CODEX_TUI_SESSION_LOG_PATH:-}"
    EMIT_DIR=""
    STATE_DIR=""
    FOLLOW=0
    QUIET=1
    DONE_PREFIX="TASKMASTER_DONE"
    POLL_INTERVAL="1"
    FOLLOW_LATEST_DIR=""
    LATEST_GLOB="taskmaster-session-*.jsonl"
    
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --log)
          LOG_PATH="${2:-}"
          shift 2
          ;;
        --emit-dir)
          EMIT_DIR="${2:-}"
          shift 2
          ;;
        --state-dir)
          STATE_DIR="${2:-}"
          shift 2
          ;;
        --follow)
          FOLLOW=1
          shift
          ;;
        --follow-latest-dir)
          FOLLOW_LATEST_DIR="${2:-}"
          shift 2
          ;;
        --latest-glob)
          LATEST_GLOB="${2:-}"
          shift 2
          ;;
        --quiet)
          QUIET=1
          shift
          ;;
        -h|--help)
          usage
          exit 0
          ;;
        *)
          echo "Unknown argument: $1" >&2
          usage
          exit 4
          ;;
      esac
    done
    
    if [[ -z "$LOG_PATH" ]]; then
      echo "Missing --log (or CODEX_TUI_SESSION_LOG_PATH)." >&2
      exit 4
    fi
    
    if [[ -z "$EMIT_DIR" ]]; then
      echo "Missing --emit-dir." >&2
      exit 4
    fi
    
    if ! command -v jq >/dev/null 2>&1; then
      echo "jq is required." >&2
      exit 4
    fi
    
    mkdir -p "$EMIT_DIR"
    STATE_FILE=""
    if [[ -n "$STATE_DIR" ]]; then
      mkdir -p "$STATE_DIR"
      STATE_FILE="$STATE_DIR/injector-state.env"
    fi
    RUNTIME_LOG="${TASKMASTER_RUNTIME_LOG:-}"
    
    SESSION_ID=""
    DONE_FOUND=0
    SESSION_ENDED=0
    TASK_COMPLETE_COUNT=0
    INJECTION_COUNT=0
    LAST_HANDLED_TURN_ID=""
    LAST_HANDLED_SIG=""
    CURRENT_LOG_PATH=""
    OFFSET=0
    PENDING_PARTIAL_LINE=""
    
    log_runtime() {
      local message="$1"
      [[ -n "$RUNTIME_LOG" ]] || return 0
      mkdir -p "$(dirname "$RUNTIME_LOG")"
      printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$message" >>"$RUNTIME_LOG"
    }
    
    save_state() {
      [[ -n "$STATE_FILE" ]] || return 0
      # Best-effort: state dir may already be cleaned up by parent
      [[ -d "$(dirname "$STATE_FILE")" ]] || return 0
      cat >"$STATE_FILE" <<EOF
    LOG_PATH=$(printf '%q' "$LOG_PATH")
    CURRENT_LOG_PATH=$(printf '%q' "$CURRENT_LOG_PATH")
    OFFSET=$(printf '%q' "$OFFSET")
    SESSION_ID=$(printf '%q' "$SESSION_ID")
    DONE_FOUND=$(printf '%q' "$DONE_FOUND")
    SESSION_ENDED=$(printf '%q' "$SESSION_ENDED")
    TASK_COMPLETE_COUNT=$(printf '%q' "$TASK_COMPLETE_COUNT")
    INJECTION_COUNT=$(printf '%q' "$INJECTION_COUNT")
    LAST_HANDLED_TURN_ID=$(printf '%q' "$LAST_HANDLED_TURN_ID")
    LAST_HANDLED_SIG=$(printf '%q' "$LAST_HANDLED_SIG")
    EOF
    }
    
    load_state() {
      [[ -n "$STATE_FILE" && -f "$STATE_FILE" ]] || return 0
      # shellcheck disable=SC1090
      source "$STATE_FILE"
    }
    
    on_exit() {
      local rc=$?
      save_state 2>/dev/null || true
      log_runtime "injector_exit rc=${rc} current_log=${CURRENT_LOG_PATH:-$LOG_PATH} session_id=${SESSION_ID:-} task_completes=${TASK_COMPLETE_COUNT} injections=${INJECTION_COUNT} session_ended=${SESSION_ENDED}" 2>/dev/null || true
    }
    
    trap on_exit EXIT
    load_state
    log_runtime "injector_start log=${LOG_PATH} follow=${FOLLOW} latest_dir=${FOLLOW_LATEST_DIR:-} state_file=${STATE_FILE:-}"
    
    build_reprompt() {
      local sid="$1"
      local token
      local shared_prompt
    
      if [[ -n "$sid" && "$sid" != "null" ]]; then
        token="${DONE_PREFIX}::${sid}"
      else
        token="${DONE_PREFIX}::<session_id>"
      fi
    
      shared_prompt="$(build_taskmaster_compliance_prompt "$token")"
    
      cat <<RE-PROMPT
    TASKMASTER: Stop is blocked until completion is explicitly confirmed.
    
    ${shared_prompt}
    RE-PROMPT
    }
    
    is_done_text() {
      local text="$1"
      [[ -n "$text" ]] || return 1
    
      if [[ -n "$SESSION_ID" ]]; then
        [[ "$text" == *"${DONE_PREFIX}::${SESSION_ID}"* ]]
      else
        [[ "$text" == *"${DONE_PREFIX}::"* ]]
      fi
    }
    
    clear_pending_prompts() {
      rm -f "$EMIT_DIR"/inject.*.txt
    }
    
    mark_done() {
      DONE_FOUND=1
    }
    
    inject_prompt() {
      local turn_id="$1"
      local sid_for_prompt="$2"
      local prompt_file
      local prompt
    
      prompt="$(build_reprompt "$sid_for_prompt")"
    
      prompt_file="$(mktemp "$EMIT_DIR/inject.XXXXXX")"
      mv "$prompt_file" "$prompt_file.txt"
      prompt_file="$prompt_file.txt"
      printf '%s' "$prompt" > "$prompt_file"
    
      INJECTION_COUNT=$((INJECTION_COUNT + 1))
      log_runtime "queued continuation prompt turn=${turn_id:-<unknown>} count=${INJECTION_COUNT} file=${prompt_file}"
      if [[ "$QUIET" -eq 0 ]]; then
        echo "[TASKMASTER] queued continuation prompt for turn ${turn_id:-<unknown>} (count=${INJECTION_COUNT}, file=${prompt_file})." >&2
      fi
    }
    
    process_line() {
      local line="$1"
      [[ -n "$line" ]] || return 0
    
      local kind msg_type sid thread_id turn_id msg_text sig
    
      kind="$(jq -Rr 'fromjson? | .kind // empty' <<<"$line" 2>/dev/null || true)"
      [[ -n "$kind" ]] || return 0
    
      case "$kind" in
        codex_event)
          msg_type="$(jq -Rr 'fromjson? | .payload.msg.type // empty' <<<"$line" 2>/dev/null || true)"
          case "$msg_type" in
            session_configured)
              sid="$(

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 withtaskmaster

Taskmaster is a completion guard for coding agents. It addresses a common failure mode: the agent makes partial progress, writes a summary, and stops before the user goal is actually finished.

Get the whole plugin
Stats
533
Stars
43
Forks
Quiet
Maintenance
Shell
Language
MIT
License
7mo ago
Last commit
8mo ago
Created
7d ago
Added

Repo: blader/taskmaster