Read the script
#!/usr/bin/env bash
#
# foundry-cli-guard.sh
#
# PreToolUse hook that flags Foundry CLI commands missing --no-prompt,
# flags manual directory/file creation that should use the CLI, and
# reminds Claude to confirm resource names with the user before creating.
#
# Prevents common failures:
# 1. Running Foundry CLI commands without --no-prompt (causes Error: EOF)
# 2. Running foundry apps deploy without --change-type (causes 500 error)
# 3. Running ui extensions create without --sockets (interactive picker hangs)
# 4. Using mkdir/touch to create app structure (causes invalid manifests)
# 5. Creating resources without user confirmation of the name
# 6. Deleting an AI agent or knowledge base without user confirmation
#
# Receives JSON on stdin with hook_event_name and tool-specific fields.
# Outputs JSON with additionalContext (advisory nudge, not blocking).
#
# Environment variables:
# FOUNDRY_SKIP_NAME_CONFIRM=1 Bypass name confirmation (for automated tests)
#
# Note: foundry-skill-router.sh also fires on `api-integrations create` for
# OpenAPI spec adaptation. Both hooks produce independent advisories.
set -euo pipefail
# shellcheck disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/host-output.sh"
INPUT=$(cat)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
case "$HOOK_EVENT" in
preToolUse) HOOK_EVENT=PreToolUse ;;
esac
if [ "$HOOK_EVENT" != "PreToolUse" ]; then
exit 0
fi
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
# Claude's shell tool is Bash. Cursor's is Shell.
if [ "$TOOL_NAME" != "Bash" ] && [ "$TOOL_NAME" != "Shell" ]; then
exit 0
fi
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# The parts of COMMAND that actually invoke the Foundry CLI: heredoc bodies and
# quoted strings are blanked, the rest is split on ; & | ( ) ` and newlines, and
# only pieces whose first word is `foundry` are kept. Subcommand checks match
# against this, so `git commit -m "... foundry apps create ..."` doesn't trigger them.
FOUNDRY_CMDS=$(printf '%s' "$COMMAND" | jq -Rrs "$(cat <<'JQ'
gsub("<<-?[[:space:]]*[\"']?(?<w>[A-Za-z_][A-Za-z0-9_]*)[\"']?[^\n]*\n(?:.*?\n)??[[:space:]]*\\k<w>(?=[[:space:]]|\\)|$)"; ""; "s")
| gsub("\"(?:\\\\.|[^\"\\\\])*\""; "\"\"")
| gsub("'[^']*'"; "''")
| [splits("[\n;&|()\\x60]+")
| sub("^[[:space:]]*(?:[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*"; "")
| select(test("^(?:[^[:space:]]*/)?foundry(?:[[:space:]]|$)"))]
| join("\n")
JQ
)")
# The same foundry pieces with their argument values intact, for reading --name,
# --sockets, and schema paths. Flag-presence checks use FOUNDRY_CMDS, so a quoted
# "--no-prompt" in a commit message can't satisfy a real foundry command.
FOUNDRY_RAW=$(printf '%s\n' "$COMMAND" | awk '{ gsub(/&&|\|\||;|\|/, "\n"); print }' |
sed -E 's/^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*//' |
grep -E '^([^[:space:]]*/)?foundry([[:space:]]|$)' || true)
# A --help or -h call only prints usage, so it never needs --no-prompt, --sockets,
# or any other flag the checks below look for.
HELP_RE='(^|[[:space:]])(-h|--help)([[:space:]]|$)'
FOUNDRY_CMDS=$(printf '%s\n' "$FOUNDRY_CMDS" | grep -vE "$HELP_RE" || true)
FOUNDRY_RAW=$(printf '%s\n' "$FOUNDRY_RAW" | grep -vE "$HELP_RE" || true)
# True when a Foundry invocation matching $1 lacks flag $2. Each line of
# FOUNDRY_CMDS is one invocation, so a flag on one command in a chain can't
# satisfy another.
missing_flag() {
printf '%s\n' "$FOUNDRY_CMDS" | grep -E "$1" | grep -vqF -- "$2"
}
# Set by reminders that must not short-circuit a later, more important advisory.
# Whichever advisory fires next prepends it; if none does, it is flushed at the end.
PENDING_REMINDER=""
# Check for Foundry CLI commands that need --no-prompt
# Nearly all Foundry CLI commands support --no-prompt:
# apps create/validate/release/delete/logs, functions create, collections create,
# workflows create, api-integrations create, agents create/delete,
# knowledge-bases create/delete (alias: kb), ui pages create, ui extensions create,
# rtr-scripts create, profile create/delete
# functions exec (incl. exec list / exec status), functions logs, functions test
if missing_flag 'foundry\s+apps\b.*\b(create|validate|release|delete|logs)\b|foundry\s+(functions|collections|workflows|api-integrations|rtr-scripts)\b.*\bcreate\b|foundry\s+(agents|knowledge-bases|kb)\b.*\b(create|delete)\b|foundry\s+functions\s+(exec|logs|test)\b|foundry\s+profile\b.*\b(create|delete)\b|foundry\s+ui\s+(pages|extensions)\b.*\bcreate\b' '--no-prompt'; then
emit_advisory "PreToolUse" "The command is missing --no-prompt. Foundry CLI commands (create/validate/release/delete, functions exec/logs/test) run non-interactively in coding assistants and will hang with Error: EOF without it. Add --no-prompt before retrying. Example: foundry apps create --name \"app-name\" --no-prompt"
exit 0
fi
# Check for foundry apps deploy without --change-type
# Omitting --change-type causes a 500 error (server-side panic) because the
# Foundry API requires a change_type field in deploy requests.
if echo "$FOUNDRY_CMDS" | grep -qE 'foundry\s+apps\s+deploy\b'; then
if missing_flag 'foundry\s+apps\s+deploy\b' '--change-type'; then
emit_advisory "PreToolUse" "The command is missing --change-type. Foundry apps deploy requires --change-type and --change-log to avoid a 500 error. Add both flags before retrying. Example: foundry apps deploy --change-type Patch --change-log \"description of changes\" --no-prompt"
exit 0
fi
if missing_flag 'foundry\s+apps\s+deploy\b' '--change-log'; then
emit_advisory "PreToolUse" "The command is missing --change-log. Foundry apps deploy requires --change-type and --change-log. Add both flags before retrying. Example: foundry apps deploy --change-type Patch --change-log \"description of changes\" --no-prompt"
exit 0
fi
fi
# Check for foundry knowledge-bases create without --files
# A knowledge base must ship at least one file. With -