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.
Install
$ npx -y skills add blader/taskmaster --agent claude-codeShips 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

