Hooks
What shipyard runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /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.
- Matches
startup|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
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.
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.
Repo: lgbarn/shipyard

