Set up foundry on new machine:
| Action | What happens |
|---|---|
Detect Python 3.10+ (python / py -3 / python3); install ~/.local/bin/python shim if needed | ✓ |
Merge statusLine, permissions.allow, enabledPlugins → ~/.claude/settings.json | ✓ |
rules/*.md → ~/.claude/rules/ | symlink |
TEAM_PROTOCOL.md → ~/.claude/ | symlink |
skills/* → ~/.claude/skills/ | symlink |
hooks/hooks.json | auto — plugin system |
| Conflict review before overwriting existing user files | ✓ |
Why symlink rules and skills (not copy)? Rules, TEAM_PROTOCOL.md, and skills load at session startup. Symlinks = every session gets plugin's current version — no stale copies, no re-run after upgrades. Broken symlink after upgrade = obvious error; stale copy silently serves old content.
Why symlink skills explicitly? claude plugin install creates ~/.claude/skills/ symlinks on first install but does NOT update them on upgrade — old version directory stays in cache, symlinks go stale. Setup's stale-version detection (same pattern as rules) replaces them silently on every re-run.
Why not symlink agents? Agents must always use full plugin prefix (foundry:sw-engineer, not sw-engineer) for unambiguous dispatch. Plugin system exposes agents at foundry: namespace — no ~/.claude/agents/ symlinks needed. (Stale agent symlinks from prior installs are removed by setup's Phase 1 cleanup.)
Why hooks need no action? hooks/hooks.json inside plugin registers automatically when plugin enabled. Setup's only hook-adjacent step: write statusLine.command path (Step 4) — statusLine is top-level settings key, not part of hooks.json.
NOT for: editing project .claude/settings.json.
- No arguments — interactive mode; prompts on conflicts.
--approve— non-interactive mode; auto-accepts all recommended answers. Use for scripted or CI setups.
Flag detection
Parse $ARGUMENTS for --approve (case-insensitive). If found, set APPROVE_ALL=true; else APPROVE_ALL=false.
Early git repository check — Step 6 requires a git repository. In --approve mode there is no interactive fallback, so check immediately before Step 1:
if [ "$APPROVE_ALL" = "true" ] && [ ! -e ".git" ]; then
printf "! --approve requires git repository — run from project root\n"
exit 1
fi
When APPROVE_ALL=true, every AskUserQuestion below skipped — ★ recommended option applied automatically. Print [--approve] auto-accepting recommended option in place of question.
Unsupported flag check — after all supported flags extracted, scan $ARGUMENTS for remaining --<token> tokens. If found: print ! Unknown flag(s): \--<token>`. Supported: `--approve`.then invokeAskUserQuestion` — (a) Abort (stop, re-invoke with correct flags) · (b) Continue ignoring (skip unknown flags, proceed). On Abort: stop.
Python detection
Probe Python 3.10+ — required before any bin/*.py calls. Windows Store stub returns exit 9009 when given args; caught by 2>/dev/null:
PYTHON_CMD=""
SHIM_DIR="$HOME/.local/bin"
if command -v python >/dev/null 2>&1 && python --version 2>/dev/null | grep -qE "Python 3\.(1[0-9]|[2-9][0-9])"; then
PYTHON_CMD="python"
elif command -v py >/dev/null 2>&1 && py -3 --version 2>/dev/null | grep -qE "Python 3\.(1[0-9]|[2-9][0-9])"; then
PYTHON_CMD="py -3"
mkdir -p "$SHIM_DIR"
printf '#!/usr/bin/env bash\npy -3 "$@"\n' > "$SHIM_DIR/python"
chmod +x "$SHIM_DIR/python"
printf " Python shim installed: %s/python → py -3\n" "$SHIM_DIR"
elif command -v python3 >/dev/null 2>&1 && python3 --version 2>/dev/null | grep -qE "Python 3\.(1[0-9]|[2-9][0-9])"; then
PYTHON_CMD="python3"
mkdir -p "$SHIM_DIR"
printf '#!/usr/bin/env bash\npython3 "$@"\n' > "$SHIM_DIR/python"
chmod +x "$SHIM_DIR/python"
printf " Python shim installed: %s/python → python3\n" "$SHIM_DIR"
else
printf "! Python 3.10+ not found — install Python 3.10+ and re-run /foundry:setup\n"
exit 1
fi
printf " Python: %s\n" "$PYTHON_CMD"
# PATH guidance — ~/.local/bin standard on modern macOS/Linux (XDG Base Directory spec) but not always on PATH by default
if [ -f "$SHIM_DIR/python" ] && ! echo ":$PATH:" | grep -q ":$SHIM_DIR:"; then
printf " ⚠ %s not on PATH — add to shell rc:\n export PATH=\"\$HOME/.local/bin:\$PATH\"\n" "$SHIM_DIR"
fi
~/.local/bin is the XDG-standard user-bin directory on modern macOS/Linux. Shim created only when python absent or resolves to Store stub. Idempotent — re-running setup overwrites shim with same content. If ~/.local/bin is not yet on $PATH, setup prints the export PATH="$HOME/.local/bin:$PATH" line for the user's shell rc.
Step 1: Locate the installed plugin
Execute this exact jq command — do not parse the JSON manually:
# Primary: registry lookup — sort by installedAt desc, pick latest install path
PLUGIN_ROOT=$(jq -r '
.plugins
| to_entries[]
| select(.key | ascii_downcase | contains("foundry"))
| .value[]
| select(.installPath != null)
| [.installedAt, .installPath]
| @tsv
' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null \
| sort -rk1 | head -1 | cut -f2) # timeout: 5000
# Fallback: filesystem scan — skip orphaned dirs, semver-sort descending, pick latest
if [ -z "$PLUGIN_ROOT" ]; then
PLUGIN_ROOT=$(find ~/.claude/plugins/cache -maxdepth 5 -name "plugin.json" 2>/dev/null \
| xargs grep -l '"name"[[:space:]]*:[[:space:]]*"foundry"' 2>/dev/null \
| while IFS= read -r f; do
dir=$(dirname "$(dirname "$f")")
[ -f "$dir/.orphaned_at" ] && continue
echo "$dir"
done \
| sort -Vr | head -1) # timeout: 10000
[ -n "$PLUGIN_ROOT" ] && printf " Note: foundry not in installed_plugins.json — using cache scan result; consider reinstalling\n"
fi
echo "$PLUGIN_ROOT" > "${TMPDIR:-/tmp}/setup-plugin-root" # persist for later blocks (Check 41)
If $PLUGIN_ROOT empty after both attempts, stop and report: "foundry plugin not found — install it first with: claude plugin marketplace add /path/to/Borda-AI-Rig && claude plugin install foundry@borda-ai-rig"
Confirm $PLUGIN_ROOT/hooks/statusline.js exists. If not, stop and report.
Step 2: Back up settings.json
If ~/.claude/settings.json does not exist, create it using the Write tool with content {}.
SETUP_BAK_TS=$(date -u +%Y%m%dT%H%M%SZ)
cp ~/.claude/settings.json "$HOME/.claude/settings.json.bak-${SETUP_BAK_TS}" # timeout: 5000
echo "$SETUP_BAK_TS" > "${TMPDIR:-/tmp}/foundry-setup-bak-ts"
Report: "Backed up ~/.claude/settings.json → ~/.claude/settings.json.bak-<timestamp>"
Step 3: Check for stale hooks block
jq -e 'has("hooks")' ~/.claude/settings.json >/dev/null 2>&1 # timeout: 5000
If hooks key exists, user has pre-plugin-migration settings block — hooks fire twice.
If APPROVE_ALL=true: print [--approve] auto-accepting: remove stale hooks block and proceed to remove (apply option a below).
Otherwise, use AskUserQuestion:
- a) Remove stale
hooksblock now ★ recommended (backup in place from Step 2) - b) Skip — I'll handle manually
On (a): use jq to strip hooks key, write back with Write tool, continue. On (b): warn "Double-firing risk: existing hooks block will fire alongside plugin-registered hooks." Continue.
Step 4: Merge statusLine
Check if statusLine already points to the current plugin's statusline.js (filename match alone is insufficient — a stale entry from an older plugin version survives upgrades and silently runs the previous hook). Verify both that the command contains statusline.js AND that the $PLUGIN_ROOT path (with its version segment) appears in the command string:
PLUGIN_ROOT=$(cat "${TMPDIR:-/tmp}/setup-plugin-root" 2>/dev/null) # reload (Che