Hooks
What ultraship runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add Houseofmvps/ultraship > /plugin install ultraship@ultraship
Ships with ultraship. 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.
bash "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
bash "${CLAUDE_PLUGIN_ROOT}/hooks/currency-guard.sh"
PostCompact
bash "${CLAUDE_PLUGIN_ROOT}/hooks/post-compact.sh"
Where it lives
- hooks/currency-guard.shRunsGitHub
Read the script
#!/bin/bash # Ultraship Currency Guard — UserPromptSubmit hook # # Models answer version-sensitive questions from training data that may be # months or years stale. This hook fires on every prompt, detects when the # prompt is about something whose correct answer changes over time (library # APIs, versions, pricing, model IDs, "latest" anything), and injects a # deterministic directive telling Claude to verify against current sources # (context7 for library docs, WebSearch/WebFetch for everything else) instead # of relying on training data. # # This is a reminder, not a block — it exits 0 and only adds context when a # currency-sensitive trigger is present, so normal prompts are untouched. # Read the hook payload from stdin (JSON with a `prompt` field). payload=$(cat) # Extract the prompt text. Prefer python3 for robust JSON parsing; fall back to # a sed extraction if python3 is unavailable. prompt="" if command -v python3 >/dev/null 2>&1; then prompt=$(printf '%s' "$payload" | python3 -c 'import sys,json try: print(json.load(sys.stdin).get("prompt","")) except Exception: pass' 2>/dev/null) fi if [ -z "$prompt" ]; then # Fallback: grab the value of the first "prompt": "..." occurrence. prompt=$(printf '%s' "$payload" | sed -n 's/.*"prompt"[[:space:]]*:[[:space:]]*"\(.*\)/\1/p' | head -c 4000) fi # Lowercase for matching. lc=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]') # If the prompt is empty, do nothing. if [ -z "$lc" ]; then exit 0 fi # Currency-sensitive triggers. Two buckets: # 1. Generic recency/version words. # 2. Library/framework/tool/SDK/API discussion, which is version-sensitive. # Word-boundary-ish matching via grep -E. Keep this fast and conservative — # false positives just add a short reminder, false negatives miss enforcement. generic_re='(latest|newest|current|up[ -]?to[ -]?date|recently|this year|2025|2026|deprecat|breaking change|migrat|upgrade|release notes|changelog)' versionish_re='\bv?[0-9]+\.[0-9x]+' techterm_re='(api|sdk|cli|library|framework|package|npm|pypi|crate|gem|endpoint|model id|model name|pricing|price|cost per|rate limit|docs|documentation|install|config|version)' # Named ecosystems that move fast (high-signal). Extend freely. named_re='(next\.?js|react|vue|svelte|astro|hono|express|fastify|nest|drizzle|prisma|tailwind|shadcn|vite|bun|deno|node|typescript|stripe|polar|supabase|vercel|railway|cloudflare|openai|anthropic|claude|gpt|gemini|llama|langchain|playwright|puppeteer)' inject=false reason="" if printf '%s' "$lc" | grep -Eq "$generic_re"; then inject=true reason="recency/version language" elif printf '%s' "$lc" | grep -Eq "$versionish_re"; then inject=true reason="a specific version number" elif printf '%s' "$lc" | grep -Eq "$named_re" && printf '%s' "$lc" | grep -Eq "$techterm_re"; then inject=true reason="a fast-moving library/tool" fi if [ "$inject" != true ]; then exit 0 fi # Build the directive. Single line, escaped for JSON. msg="Ultraship Currency Guard: this prompt touches ${reason}, where training data is often stale. BEFORE answering, verify against current sources — use the context7 MCP (resolve-library-id then query-docs) for any library/framework/SDK API, and WebSearch/WebFetch for versions, pricing, model IDs, release notes, or anything time-sensitive. Do not state version-specific facts, API signatures, or prices from memory. If you cannot verify, say so explicitly rather than guessing." # Escape backslashes and double quotes for valid JSON. esc=$(printf '%s' "$msg" | sed 's/\\/\\\\/g; s/"/\\"/g') printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}' "$esc" exit 0 - hooks/guard-bash.shGitHub
Read the script
#!/bin/bash # Ultraship Guard — PreToolUse hook for Bash commands # Blocks destructive commands and warns before execution # Reads the tool input from stdin (JSON with "input" field containing the command) INPUT=$(cat) # Extract the command value from JSON — handle escaped quotes # Use grep to get the "command":"value" pair, then strip the key prefix and trailing quote+brace COMMAND=$(printf '%s' "$INPUT" | tr -d '\n' | sed 's/.*"command"[[:space:]]*:[[:space:]]*"//;s/"[[:space:]]*[,}].*//' | sed 's/\\"/"/g') if [ -z "$COMMAND" ]; then # No command found — allow exit 0 fi # Destructive patterns to block BLOCKED=false REASON="" # rm -rf anywhere in the command (catches piped, chained, and direct usage) if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive)'; then BLOCKED=true REASON="Destructive recursive removal (rm -rf)" fi # DROP TABLE / DROP DATABASE / TRUNCATE if echo "$COMMAND" | grep -qiE '(DROP\s+(TABLE|DATABASE)|TRUNCATE\s+TABLE)'; then BLOCKED=true REASON="SQL destructive operation — DROP/TRUNCATE" fi # git push --force to main/master if echo "$COMMAND" | grep -qE 'git\s+push\s+(-[a-zA-Z]*f|--force).*\s+(main|master)'; then BLOCKED=true REASON="Force-push to main/master branch" fi # git reset --hard if echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard'; then BLOCKED=true REASON="git reset --hard — discards all uncommitted changes" fi # git checkout . (discard all changes) if echo "$COMMAND" | grep -qE 'git\s+checkout\s+\.$'; then BLOCKED=true REASON="git checkout . — discards all working directory changes" fi # git clean -f if echo "$COMMAND" | grep -qE 'git\s+clean\s+-[a-zA-Z]*f'; then BLOCKED=true REASON="git clean -f — permanently removes untracked files" fi # git branch -D (force delete) if echo "$COMMAND" | grep -qE 'git\s+branch\s+-D\s'; then BLOCKED=true REASON="git branch -D — force-deletes branch without merge check" fi # kubectl delete if echo "$COMMAND" | grep -qE 'kubectl\s+delete'; then BLOCKED=true REASON="kubectl delete — removes Kubernetes resources" fi # docker system prune / docker volume rm if echo "$COMMAND" | grep -qE 'docker\s+(system\s+prune|volume\s+rm)'; then BLOCKED=true REASON="Docker destructive operation" fi # git restore . (discard all working directory changes) if echo "$COMMAND" | grep -qE 'git\s+restore\s+\.$'; then BLOCKED=true REASON="git restore . — discards all working directory changes" fi # base64 decode piped to shell (encoded destructive commands) if echo "$COMMAND" | grep -qE 'base64\s+(-d|--decode).*\|\s*(ba)?sh'; then BLOCKED=true REASON="base64-encoded command piped to shell — potential destructive payload" fi # curl/wget piped to shell (remote code execution) if echo "$COMMAND" | grep -qE '(curl|wget)\s.*\|\s*(ba)?sh'; then BLOCKED=true REASON="Remote script piped to shell — potential code execution" fi # Python/Perl destructive one-liners if echo "$COMMAND" | grep -qE 'python[23]?\s+-c\s.*\b(rmtree|unlink|remove)\b'; then BLOCKED=true REASON="Python destructive filesystem operation" fi if echo "$COMMAND" | grep -qE 'perl\s+-e\s.*\b(rmtree|unlink)\b'; then BLOCKED=true REASON="Perl destructive filesystem operation" fi # xargs with destructive git/rm commands if echo "$COMMAND" | grep -qE 'xargs\s.*\b(rm\s+-rf|git\s+push\s+--force)'; then BLOCKED=true REASON="xargs chaining destructive command" fi if [ "$BLOCKED" = true ]; then echo "⚠️ GUARD BLOCKED: $REASON" echo "Command: $COMMAND" echo "To proceed, explicitly confirm this action." exit 2 fi exit 0
- hooks/guard-edit.shGitHub
Read the script
#!/bin/bash # Ultraship Guard — PreToolUse hook for Edit/Write commands # Blocks edits outside the frozen directory (if set) # Reads tool input from stdin FREEZE_FILE="${PWD}/.ultraship/guard-freeze.txt" # If no freeze file, allow all edits if [ ! -f "$FREEZE_FILE" ]; then exit 0 fi FREEZE_DIR=$(cat "$FREEZE_FILE" 2>/dev/null | head -1 | tr -d '[:space:]') if [ -z "$FREEZE_DIR" ]; then exit 0 fi # Extract file_path from the input JSON INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') if [ -z "$FILE_PATH" ]; then exit 0 fi # Resolve to absolute path for comparison RESOLVED_FREEZE=$(cd "$PWD" && realpath "$FREEZE_DIR" 2>/dev/null || echo "$PWD/$FREEZE_DIR") RESOLVED_FILE=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH") # Check if the file is within the frozen directory case "$RESOLVED_FILE" in "$RESOLVED_FREEZE"/*) # File is within the allowed directory exit 0 ;; "$RESOLVED_FREEZE") # File is the directory itself (unlikely but safe) exit 0 ;; *) echo "⚠️ GUARD BLOCKED: Edit outside frozen directory" echo "File: $FILE_PATH" echo "Allowed directory: $FREEZE_DIR" echo "To edit files outside this directory, run /unfreeze first." exit 2 ;; esac - hooks/post-compact.shRunsGitHub
Read the script
#!/bin/bash # Ultraship PostCompact hook # Re-injects essential context after conversation compaction # This prevents ultraship state from being lost in long sessions CONTEXT="" # Check if guard is active FREEZE_FILE="${PWD}/.ultraship/guard-freeze.txt" if [ -f "$FREEZE_FILE" ]; then FREEZE_DIR=$(cat "$FREEZE_FILE" 2>/dev/null | head -1 | tr -d '[:space:]') if [ -n "$FREEZE_DIR" ]; then CONTEXT="Ultraship Guard is ACTIVE — edits restricted to: ${FREEZE_DIR}. Run /guard to manage." fi fi # Remind about available Ultraship commands CONTEXT="${CONTEXT}\\nUltraship plugin is active. Key commands: /ship (pre-deploy audit), /seo (SEO audit), /pentest (security test), /guard (safety), /sprint (workflow), /investigate (debug), /rescue (incidents), /compete (competitor analysis), /seo-strategy (elite SEO), /canary (post-deploy check), /retro (retrospective), /learn (project knowledge)." # Check CLAUDE.md freshness CLAUDE_MD="$PWD/CLAUDE.md" if [ -f "$CLAUDE_MD" ]; then if [ "$(uname)" = "Darwin" ]; then mod_epoch=$(stat -f %m "$CLAUDE_MD") else mod_epoch=$(stat -c %Y "$CLAUDE_MD") fi now_epoch=$(date +%s) age_days=$(( (now_epoch - mod_epoch) / 86400 )) if [ "$age_days" -ge 7 ]; then CONTEXT="${CONTEXT}\\nCLAUDE.md is ${age_days} days old — consider /revise-claude-md." fi fi # Output as hookSpecificOutput msg=$(printf '%s' "$CONTEXT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g') printf '{"hookSpecificOutput":{"hookEventName":"PostCompact","additionalContext":"%s"}}' "$msg" - hooks/session-start.shRunsGitHub
Read the script
#!/bin/bash # Ultraship SessionStart hook # Checks CLAUDE.md existence/freshness and enforces memory-first behavior CLAUDE_MD="$PWD/CLAUDE.md" CONTEXT="" # Helper: safely output JSON with escaped strings json_context() { local msg="$1" # Escape backslashes, double quotes, and control characters for valid JSON msg=$(printf '%s' "$msg" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g') printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}' "$msg" } # --- Memory check --- # Look for MEMORY.md in common locations MEMORY_LOCATIONS=( "$HOME/.claude/projects/$(echo "$PWD" | tr '/' '-')/memory/MEMORY.md" "$PWD/.claude/memory/MEMORY.md" "$HOME/.claude/MEMORY.md" ) MEMORY_FOUND=false for loc in "${MEMORY_LOCATIONS[@]}"; do if [ -f "$loc" ]; then MEMORY_FOUND=true break fi done if [ "$MEMORY_FOUND" = true ]; then CONTEXT="IMPORTANT: Read MEMORY.md and relevant memory files BEFORE performing any task. This ensures persistent context across sessions. Never skip this step." else CONTEXT="No memory files found. Consider setting up auto-memory (MEMORY.md) for persistent context across sessions." fi # --- CLAUDE.md check --- if [ ! -f "$CLAUDE_MD" ]; then CONTEXT="${CONTEXT}\\nNo CLAUDE.md found in this project directory (${PWD}). Offer to create one based on the project structure — check package.json, directory layout, and any existing README for context." json_context "$CONTEXT" exit 0 fi if [ "$(uname)" = "Darwin" ]; then mod_epoch=$(stat -f %m "$CLAUDE_MD") else mod_epoch=$(stat -c %Y "$CLAUDE_MD") fi now_epoch=$(date +%s) age_days=$(( (now_epoch - mod_epoch) / 86400 )) if [ "$age_days" -ge 7 ]; then CONTEXT="${CONTEXT}\\nCLAUDE.md in this project is ${age_days} days old. Consider running /revise-claude-md to keep it current." fi json_context "$CONTEXT"
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.
"ULTRASHIP" Claude Code plugin — 39 skills, 33 tools, 11 agents for ship-ready workflows: planning, review, pentesting, safety guardrails, canary monitoring, SEO/AI-readiness check, penetration testing, code review, competitive analysis, incident response. 1 dependency. 180 tests. MIT.
Repo: Houseofmvps/ultraship

