/ship Workflow
Inputs
- Plan file: $ARGUMENTS # e.g. ./plans/feature-x.md
Role
You are the work coordinator. You dispatch work to agents and check their results. You do NOT write code, explore the codebase, or run tests yourself — agents do that. Your job: read the plan once, dispatch each step, check verdicts, gate progression.
Step 0 — Pre-flight checks
Tool: Bash (git status, cleanup), Glob (agent checks) — Run all checks in parallel in a single message
Parse --security-override flag (MUST be first action in Step 0):
If $ARGUMENTS contains --security-override:
- Extract the reason string (quoted text after
--security-override) - Store as
$SECURITY_OVERRIDE_REASON - Remove the flag and reason from
$ARGUMENTSbefore using it as the plan path - After extraction,
$ARGUMENTScontains ONLY the plan path for all subsequent steps - Log: "Security override active. Reason: $SECURITY_OVERRIDE_REASON"
If $ARGUMENTS does not contain --security-override:
- Set
$SECURITY_OVERRIDE_REASONto empty
First: Generate a unique run ID for this invocation
Tool: Bash
RUN_ID=$(date +%Y%m%d-%H%M%S)-$(cat /dev/urandom | LC_ALL=C tr -dc 'a-z0-9' | head -c 6)
echo "Ship run ID: $RUN_ID"
Then: Clean up stale artifacts from previous runs
Tool: Bash
# Prune orphaned worktrees
git worktree prune 2>/dev/null || true
# Clean up orphaned tracking files from aborted runs
for tracking_file in .ship-worktrees-*.tmp; do
[ -f "$tracking_file" ] || continue
ORPHANED=true
while IFS='|' read -r wt_path _rest; do
if git worktree list --porcelain | grep -q "^worktree $wt_path$"; then
ORPHANED=false
break
fi
done < "$tracking_file"
if $ORPHANED; then
rm -f "$tracking_file"
echo "Cleaned up orphaned tracking file: $tracking_file"
fi
done
# Clean up orphaned violation files
rm -f .ship-violations-*.tmp
Then: Initialize audit logging
Tool: Bash
# --- Audit Logging Setup ---
AUDIT_LOG_DIR="./plans/audit-logs"
mkdir -p "$AUDIT_LOG_DIR"
AUDIT_LOG="$AUDIT_LOG_DIR/ship-${RUN_ID}.jsonl"
STATE_FILE=".ship-audit-state-${RUN_ID}.json"
# L3 (audited): generate HMAC key and persist to disk for post-run chain verification
HMAC_KEY=""
if [ "$SECURITY_MATURITY" = "audited" ]; then
HMAC_KEY=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 64 2>/dev/null || echo "")
if [ -n "$HMAC_KEY" ]; then
KEY_FILE=".ship-audit-key-${RUN_ID}"
printf '%s' "$HMAC_KEY" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
echo "L3 HMAC key written to $KEY_FILE (mode 0600)"
else
echo "Warning: Could not generate L3 HMAC key (/dev/urandom unavailable)."
fi
fi
# Create state file for helper script
python3 -c "
import json
state = {
'run_id': '${RUN_ID}',
'audit_log': '${AUDIT_LOG}',
'skill': 'ship',
'skill_version': '3.7.0',
'security_maturity': '${SECURITY_MATURITY}',
'hmac_key': '${HMAC_KEY}'
}
with open('${STATE_FILE}', 'w') as f:
json.dump(state, f)
print('Audit state file created: ${STATE_FILE}')
"
# Emit run_start event
OVERRIDE_ACTIVE="false"
[ -n "${SECURITY_OVERRIDE_REASON:-}" ] && OVERRIDE_ACTIVE="true"
bash scripts/emit-audit-event.sh "$STATE_FILE" \
"{\"event_type\":\"run_start\",\"plan_file\":\"${PLAN_PATH:-${ARGUMENTS:-unknown}}\",\"security_override_active\":${OVERRIDE_ACTIVE}}"
# Emit step_start for step_0
bash scripts/emit-audit-event.sh "$STATE_FILE" \
'{"event_type":"step_start","step":"step_0_preflight","step_name":"Pre-flight checks","agent_type":"coordinator"}'
echo "Audit log: $AUDIT_LOG"
Then: Run validation checks in parallel:
git status --porcelain(Bash)- Glob for
.claude/agents/coder*.md - Glob for
.claude/agents/code-reviewer*.md - Glob for
.claude/agents/qa-engineer*.mdor.claude/agents/qa*.md
Fail fast if any check fails:
- If git status is not empty: "❌ Working directory is not clean. Commit or stash changes before running /ship."
- If no coder agent found: "❌ No coder agent found. Generate one using:\n
python3 ~/workspaces/claude-devkit/generators/generate_agents.py . --type coder" - If no code-reviewer agent found: "❌ No code-reviewer agent found. Generate one using:\n
python3 ~/workspaces/claude-devkit/generators/generate_agents.py . --type code-reviewer" - If no qa-engineer agent found: "❌ No qa-engineer agent found. Generate one using:\n
python3 ~/workspaces/claude-devkit/generators/generate_agents.py . --type qa-engineer"
If any check fails, stop immediately and list all failures.
Security maturity level check:
Tool: Bash, Read
Read .claude/settings.local.json (if exists), then .claude/settings.json (if exists). Extract the security_maturity field. Precedence: if .claude/settings.local.json provides a security_maturity value (even if that value is "advisory"), the project-level setting is not consulted. The fallback to .claude/settings.json only occurs when the local file is absent or does not contain the security_maturity key.
Note: This block uses python3 -c for JSON parsing. Python 3 is available on all target platforms (macOS, Linux dev environments) and the json module handles edge cases (nested objects, whitespace, escaping) more reliably than regex-based alternatives. If python3 is not available, the command silently fails and the maturity level defaults to "advisory" (L1) — the safe default. This is analogous to existing /ship pre-flight checks that use git and other CLI tools.
SECURITY_MATURITY="advisory" # Default: L1
LOCAL_SET=0 # Track source, not value, to preserve precedence when local sets "advisory"
# Read local settings first (takes precedence)
if [ -f ".claude/settings.local.json" ]; then
LOCAL_MATURITY=$(python3 -c "import json; d=json.load(open('.claude/settings.local.json')); print(d.get('security_maturity',''))" 2>/dev/null || echo "")
if [ -n "$LOCAL_MATURITY" ]; then
SECURITY_MATURITY="$LOCAL_MATURITY"
LOCAL_SET=1
fi
fi
# Only fall back to project settings if local did NOT provide a value
if [ "$LOCAL_SET" -eq 0 ] && [ -f ".claude/settings.json" ]; then
PROJECT_MATURITY=$(python3 -c "import json; d=json.load(open('.claude/settings.json')); print(d.get('security_maturity',''))" 2>/dev/null || echo "")
[ -n "$PROJECT_MATURITY" ] && SECURITY_MATURITY="$PROJECT_MATURITY"
fi
# Validate value
case "$SECURITY_MATURITY" in
advisory|enforced|audited) ;;
*) echo "Warning: Invalid security_maturity value '$SECURITY_MATURITY'. Defaulting to 'advisory'."
SECURITY_MATURITY="advisory" ;;
esac
echo "Security maturity level: $SECURITY_MATURITY"
If $SECURITY_MATURITY is enforced or audited:
Tool: Glob
Check for required security skills:
- Glob
~/.claude/skills/secrets-scan/SKILL.md - Glob
~/.claude/skills/secure-review/SKILL.md - Glob
~/.claude/skills/dependency-audit/SKILL.md
If ANY are missing, stop immediately: "Security maturity level '$SECURITY_MATURITY' requires all security skills to be deployed. Missing skills:
- [list missing skills]
Deploy with: cd ~/projects/claude-devkit && ./scripts/deploy.sh secrets-scan secure-review dependency-audit"
Secrets scan gate (pre-flight):
Tool: Glob
Glob for ~/.claude/skills/secrets-scan/SKILL.md
If found:
Tool: Task, subagent_type=general-purpose, model=claude-sonnet-4-6
Prompt: "You are running a pre-commit secrets scan as part of the /ship pre-flight check.
Read the secrets-scan skill definition at ~/.claude/skills/secrets-scan/SKILL.md.
Execute it with scope all against the current repository working directory.
Report your verdict: PASS or BLOCKED. If BLOCKED, list the confirmed secret types and file locations (NO actual secret values). If PASS, report 'No secrets detected in working directory.'"
If secrets scan returns BLOCKED: Secrets-scan BLOCKED blocks at ALL maturity levels (including L1). Committed secrets cannot be un-committed and requ