Help us improve
Share bugs, ideas, or general feedback.
From foundry
Performs post-install setup for foundry plugin: merges statusLine, permissions.allow, enabledPlugins into ~/.claude/settings.json; symlinks rules/*.md and TEAM_PROTOCOL.md into ~/.claude/. Use after install on new machines.
npx claudepluginhub borda/ai-rig --plugin foundryHow this skill is triggered — by the user, by Claude, or both
Slash command
/foundry:initsonnetThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
<objective>
Documents .claude/plugin-name.local.md pattern for per-project Claude Code plugin configuration using YAML frontmatter and markdown. Guides reading settings from bash hooks and commands.
Configure allow/deny/ask permission rules in .claude/settings.json for Claude Code tools like Bash(git:*), Write, Edit. Builds layered policies with glob patterns for git commands.
Configures Claude Code permissions with allow/deny rules and a path-restriction hook for safe autonomous agent workflows.
Share bugs, ideas, or general feedback.
Set up foundry on a new machine:
| Action | What happens |
|---|---|
Merge statusLine, permissions.allow, enabledPlugins → ~/.claude/settings.json | ✓ |
rules/*.md → ~/.claude/rules/ | symlink |
TEAM_PROTOCOL.md → ~/.claude/ | symlink |
hooks/hooks.json | auto — plugin system |
| Conflict review before overwriting existing user files | ✓ |
Why symlink rules (not copy)? Rules and TEAM_PROTOCOL.md are loaded at session startup. Symlinks mean every session always gets the plugin's current version — no stale copies, no need to re-run init after upgrades. A broken symlink after an upgrade produces an obvious error; a stale copy silently serves old content.
Why not symlink agents and skills? The Claude Code plugin system already exposes all plugin skills and agents at root namespace. Agents must always be referenced with the full plugin prefix (foundry:sw-engineer, not sw-engineer) for unambiguous dispatch regardless of what symlinks exist. Init creates no agent or skill symlinks.
Why hooks need no action? hooks/hooks.json inside the plugin is registered automatically by the Claude Code plugin system when the plugin is enabled. Init's only hook-adjacent step is writing the statusLine.command path (Step 3) — because statusLine is a top-level settings key, not part of hooks.json.
NOT for: editing project .claude/settings.json.
--approve — non-interactive mode; automatically accepts all recommended answers without prompting. Use for scripted or CI-style setups.Parse $ARGUMENTS for the presence of --approve (case-insensitive). If found, set APPROVE_ALL=true; otherwise APPROVE_ALL=false.
When APPROVE_ALL=true, every AskUserQuestion call below is skipped and the ★ recommended option is applied automatically. Print [--approve] auto-accepting recommended option in place of the question.
Read ~/.claude/plugins/installed_plugins.json using the Read tool. Find the entry whose key contains foundry (case-insensitive). Extract its installPath. If the file does not exist or contains no foundry entry, fall back to a filesystem scan:
PLUGIN_ROOT=$(jq -r 'to_entries[] | select(.key | ascii_downcase | contains("foundry")) | .value.installPath // empty' \
"$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null | head -1) # timeout: 5000
# Fallback when registry entry is absent (manual cache copies, partial installs)
if [ -z "$PLUGIN_ROOT" ]; then
PLUGIN_ROOT=$(find ~/.claude/plugins/cache -maxdepth 5 -name "plugin.json" 2>/dev/null \
| xargs grep -l 'foundry' 2>/dev/null \
| head -1 \
| xargs -I{} dirname {}) # timeout: 10000
[ -n "$PLUGIN_ROOT" ] && printf " Note: foundry not in installed_plugins.json — using cache scan result; consider reinstalling\n"
fi
If $PLUGIN_ROOT is 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.
[ ! -f ~/.claude/settings.json ] && echo '{}' > ~/.claude/settings.json # timeout: 5000
cp ~/.claude/settings.json ~/.claude/settings.json.bak # timeout: 5000
Report: "Backed up ~/.claude/settings.json → ~/.claude/settings.json.bak"
jq -e 'has("hooks")' ~/.claude/settings.json >/dev/null 2>&1 # timeout: 5000
If the hooks key exists, the user has a pre-plugin-migration settings block that will cause hooks to fire twice.
If APPROVE_ALL=true: print [--approve] auto-accepting: remove stale hooks block and proceed directly to removing it (apply option a below).
Otherwise, use AskUserQuestion:
hooks block now ★ recommended (backup already in place from Step 2)On (a): use jq to strip the hooks key and write back with the Write tool, then continue. On (b): warn "Double-firing risk: existing hooks block will fire alongside plugin-registered hooks." Continue.
Check if statusLine is already pointing to statusline.js:
jq -e '(.statusLine.command // "") | contains("statusline.js")' ~/.claude/settings.json >/dev/null 2>&1 # timeout: 5000
If already set: report "statusLine already set — skipping." Otherwise, use jq to set the value:
jq --arg cmd "node \"$PLUGIN_ROOT/hooks/statusline.js\"" \
'.statusLine = {"async":true,"command":$cmd,"type":"command"}' \
~/.claude/settings.json > /tmp/foundry_init_tmp.json # timeout: 5000
Write /tmp/foundry_init_tmp.json content back to ~/.claude/settings.json using the Write tool.
Read $PLUGIN_ROOT/.claude-plugin/permissions-allow.json using the Read tool. Merge into ~/.claude/settings.json — add only entries not already present (exact string match):
jq --slurpfile perms "$PLUGIN_ROOT/.claude-plugin/permissions-allow.json" \
'.permissions.allow = ((.permissions.allow // []) + $perms[0] | unique)' \
~/.claude/settings.json > /tmp/foundry_init_tmp.json # timeout: 5000
Write back with the Write tool. Report: "Added N new permissions.allow entries (M already present)."
Check whether $PLUGIN_ROOT/.claude-plugin/permissions-deny.json exists. If it does, read it using the Read tool and merge into ~/.claude/settings.json — add only entries not already present:
jq --slurpfile deny "$PLUGIN_ROOT/.claude-plugin/permissions-deny.json" \
'.permissions.deny = ((.permissions.deny // []) + $deny[0] | unique)' \
~/.claude/settings.json > /tmp/foundry_init_tmp.json # timeout: 5000
Write back with the Write tool. Report: "Added N new permissions.deny entries (M already present)."
Copy $PLUGIN_ROOT/permissions-guide.md to .claude/permissions-guide.md — only if the destination does not already exist (preserves project-local edits made via /manage):
if [ ! -f ".claude/permissions-guide.md" ]; then # timeout: 5000
cp "$PLUGIN_ROOT/permissions-guide.md" ".claude/permissions-guide.md"
printf " copied: permissions-guide.md\n"
else
printf " permissions-guide.md already present — skipping\n"
fi
jq -e '.enabledPlugins["codex@openai-codex"] == true' ~/.claude/settings.json >/dev/null 2>&1 # timeout: 5000
If already true: report "enabledPlugins already set — skipping." Otherwise:
jq '.enabledPlugins["codex@openai-codex"] = true' \
~/.claude/settings.json > /tmp/foundry_init_tmp.json # timeout: 5000
Write back with the Write tool.
After all writes, confirm the file parses as valid JSON:
jq empty ~/.claude/settings.json # timeout: 5000
If jq exits non-zero: restore from backup (cp ~/.claude/settings.json.bak ~/.claude/settings.json), report the error, and stop. If valid: continue.
Ensure target directory exists:
mkdir -p ~/.claude/rules # timeout: 5000
Conflict scan — identify rule files and TEAM_PROTOCOL.md that exist in ~/.claude/ as real files or symlinks pointing elsewhere:
LINK_CONFLICTS=()
for src in "$PLUGIN_ROOT/rules/"*.md; do
dest="$HOME/.claude/rules/$(basename "$src")"
if [ -L "$dest" ]; then
target=$(readlink "$dest")
echo "$target" | grep -q "$PLUGIN_ROOT" || LINK_CONFLICTS+=("rules/$(basename "$src") → $target")
elif [ -f "$dest" ]; then
LINK_CONFLICTS+=("rules/$(basename "$src") (real file)")
fi
done # timeout: 5000
src="$PLUGIN_ROOT/TEAM_PROTOCOL.md"; dest="$HOME/.claude/TEAM_PROTOCOL.md"
if [ -L "$dest" ]; then
target=$(readlink "$dest")
echo "$target" | grep -q "$PLUGIN_ROOT" || LINK_CONFLICTS+=("TEAM_PROTOCOL.md → $target")
elif [ -f "$dest" ]; then
LINK_CONFLICTS+=("TEAM_PROTOCOL.md (real file)")
fi # timeout: 5000
If conflicts exist:
If APPROVE_ALL=true: print [--approve] auto-accepting: replace all symlink conflicts and proceed with replacing all (apply option a below).
Otherwise, use AskUserQuestion:
These entries in ~/.claude/ would be replaced with symlinks to the foundry plugin:
- <name> (<current state>)
- …
Options:
On c: loop with AskUserQuestion — "Replace <name>? (y) Yes / (n) Skip".
Symlink — for each approved or already-absent entry, ln -sf atomically replaces:
for src in "$PLUGIN_ROOT/rules/"*.md; do
ln -sf "$src" "$HOME/.claude/rules/$(basename "$src")" # timeout: 5000
echo " linked: $(basename "$src")"
done # timeout: 10000
ln -sf "$PLUGIN_ROOT/TEAM_PROTOCOL.md" ~/.claude/TEAM_PROTOCOL.md # timeout: 5000
echo " linked: TEAM_PROTOCOL.md"
Print summary:
Suggest: "Re-run /foundry:init after any plugin upgrade to refresh symlinks to the new cache path."
Testing init changes: The init skill has no .claude/skills/init entry — it is only reachable as /foundry:init after the plugin is installed. To test changes: bump version in plugins/foundry/.claude-plugin/plugin.json, then run claude plugin install foundry@borda-ai-rig from the repo root to refresh the cache, then invoke /foundry:init. Upgrade path: After claude plugin install foundry@borda-ai-rig upgrades the version, symlinks will point to the old cache path. Re-run /foundry:init — Step 7 detects stale symlinks as conflicts and replaces them.