Skip to content
Development
Hook

Hooks

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

From plugin
shipyard
6525 skills20 agents25 commands4 hooks
Install
> /plugin marketplace add lgbarn/shipyard
> /plugin install shipyard@shipyard

Ships with shipyard. Installing the plugin gets these hooks.

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesstartup|resume|clear|compact${CLAUDE_PLUGIN_ROOT}/scripts/state-read.sh${CLAUDE_PLUGIN_ROOT}/scripts/marketplace-sync.sh

TeammateIdle

  • ${CLAUDE_PLUGIN_ROOT}/hooks/teammate-idle.sh

TaskCompleted

  • ${CLAUDE_PLUGIN_ROOT}/hooks/task-completed.sh

Stop

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

In the plugin's words

How shipyard describes its own hook set.

Shipyard plugin hooks

Where it lives

  • hooks/stop.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Stop/SessionEnd hook: state safety on session termination
    # Detects active build status and appends interruption note to HISTORY.md.
    # Exit 0 always — this hook records, never blocks.
    
    set -euo pipefail
    
    # Kill switch: skip all hooks
    if [ "${SHIPYARD_DISABLE_HOOKS:-}" = "true" ]; then exit 0; fi
    # Selective skip: comma-separated hook names
    HOOK_NAME="$(basename "${BASH_SOURCE[0]}" .sh)"
    if [[ ",${SHIPYARD_SKIP_HOOKS:-}," == *",$HOOK_NAME,"* ]]; then exit 0; fi
    
    if [ -f ".shipyard/STATE.json" ]; then
        status=$(jq -r '.status // ""' .shipyard/STATE.json 2>/dev/null)
        if [ "$status" = "building" ]; then
            echo "- [$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Session ended during build (may need /shipyard:resume)" \
                >> .shipyard/HISTORY.md
        fi
    fi
    
    exit 0
    
  • hooks/task-completed.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # TaskCompleted hook: quality gate before marking task done
    # Exit 0 = allow completion, Exit 2 = block with feedback
    #
    # Solo mode: always allows (exit 0)
    # Teammate mode: verifies task has evidence (test output, results)
    
    set -euo pipefail
    
    # Kill switch: skip all hooks
    if [ "${SHIPYARD_DISABLE_HOOKS:-}" = "true" ]; then exit 0; fi
    # Selective skip: comma-separated hook names
    HOOK_NAME="$(basename "${BASH_SOURCE[0]}" .sh)"
    if [[ ",${SHIPYARD_SKIP_HOOKS:-}," == *",$HOOK_NAME,"* ]]; then exit 0; fi
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
    source "${SCRIPT_DIR}/scripts/team-detect.sh"
    source "${SCRIPT_DIR}/scripts/hook-log.sh"
    
    # Solo mode: skip gates
    if [ "${SHIPYARD_IS_TEAMMATE}" != "true" ]; then
        exit 0
    fi
    
    # Check for evidence: phase results or verification artifacts in .shipyard
    if [ -d ".shipyard/phases" ]; then
        evidence_count=0
        # Scope evidence check to current phase directory
        current_phase=$(jq -r '.phase // ""' .shipyard/STATE.json 2>/dev/null || true)
        if [ -n "$current_phase" ]; then
            phase_dir=$(find .shipyard/phases/ -maxdepth 1 -type d \
                \( -name "${current_phase}*" -o -name "0${current_phase}*" \) 2>/dev/null | head -1)
            if [ -n "$phase_dir" ]; then
                evidence_count=$(find "$phase_dir" \
                    \( -name "SUMMARY-*.md" -o -name "REVIEW-*.md" -o -name "AUDIT-*.md" \) \
                    | wc -l | tr -d ' ')
            fi
        else
            # Fallback: no phase in state, check all phases
            evidence_count=$(find .shipyard/phases/ \
                \( -name "SUMMARY-*.md" -o -name "REVIEW-*.md" -o -name "AUDIT-*.md" \) \
                | wc -l | tr -d ' ')
        fi
        if [ "${evidence_count}" -gt 0 ]; then
            # Verify at least one evidence file has non-trivial content (>3 lines)
            has_substance=false
            while IFS= read -r efile; do
                if [ "$(wc -l < "$efile" | tr -d ' ')" -gt 3 ]; then
                    has_substance=true
                    break
                fi
            done < <(find "${phase_dir:-.shipyard/phases/}" \
                \( -name "SUMMARY-*.md" -o -name "REVIEW-*.md" -o -name "AUDIT-*.md" \) 2>/dev/null)
            if [ "$has_substance" = true ]; then
                exit 0
            fi
            _msg="BLOCKED: Evidence files exist but none have substantive content (>3 lines). Add real verification results."
            echo "$_msg" >&2
            echo "  (see ${HOOK_LOG} for details)" >&2
            log_hook_failure "$HOOK_NAME" "2" "$_msg"
            exit 2
        fi
    fi
    
    _msg="BLOCKED: No verification evidence found. Run tests and produce results before marking task complete."
    echo "$_msg" >&2
    echo "  (see ${HOOK_LOG} for details)" >&2
    log_hook_failure "$HOOK_NAME" "2" "$_msg"
    exit 2
    
  • hooks/teammate-idle.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # TeammateIdle hook: quality gate before teammate stops
    # Exit 0 = allow idle, Exit 2 = block with feedback
    #
    # Solo mode: always allows (exit 0)
    # Teammate mode: runs version check + test pass verification
    
    set -euo pipefail
    
    # Kill switch: skip all hooks
    if [ "${SHIPYARD_DISABLE_HOOKS:-}" = "true" ]; then exit 0; fi
    # Selective skip: comma-separated hook names
    HOOK_NAME="$(basename "${BASH_SOURCE[0]}" .sh)"
    if [[ ",${SHIPYARD_SKIP_HOOKS:-}," == *",$HOOK_NAME,"* ]]; then exit 0; fi
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
    source "${SCRIPT_DIR}/scripts/team-detect.sh"
    source "${SCRIPT_DIR}/scripts/hook-log.sh"
    
    # Solo mode: skip gates
    if [ "${SHIPYARD_IS_TEAMMATE}" != "true" ]; then
        exit 0
    fi
    
    # Gate 1: Version check
    if [ -f "${SCRIPT_DIR}/scripts/check-versions.sh" ]; then
        if ! _output=$(bash "${SCRIPT_DIR}/scripts/check-versions.sh" 2>&1); then
            _msg="BLOCKED: Version check failed. Fix version mismatches before stopping."
            echo "$_msg" >&2
            echo "$_output" | tail -5 >&2
            echo "  (see ${HOOK_LOG} for details)" >&2
            log_hook_failure "$HOOK_NAME" "2" "$_msg"
            exit 2
        fi
    fi
    
    # Gate 2: Tests must pass
    if ! _output=$(npm test --prefix "${SCRIPT_DIR}" 2>&1); then
        _msg="BLOCKED: Tests are failing. Fix test failures before stopping."
        echo "$_msg" >&2
        echo "$_output" | tail -10 >&2
        echo "  (see ${HOOK_LOG} for details)" >&2
        log_hook_failure "$HOOK_NAME" "2" "$_msg"
        exit 2
    fi
    
    exit 0
    

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 withshipyard

A Claude Code plugin for structured project execution. Plan work in phases, build with parallel agents and TDD, review with security audits and quality gates, and ship with confidence.

Get the whole plugin