| name | setup-project |
| description | Bootstraps a new project directory with base configuration files, MCP servers, API connections, and template skills from OutworkOS. Use when setting up a new project, initializing a workspace, or configuring MCP integrations for a new directory. |
| argument-hint | <project-name> |
/setup-project
Bootstrap a new project directory under $OUTWORKOS_PARENT with base configuration files from $OUTWORKOS_ROOT (the OutworkOS repo — source of truth for shared skills and credentials).
Key concepts:
$OUTWORKOS_ROOT — the OutworkOS repo (where you run Claude from, contains scripts/, skills/, .mcp.json)
$OUTWORKOS_HOME — data directory (e.g., ~/.outworkos)
$OUTWORKOS_PARENT — where project folders live (e.g., ~/.outworkos/projects/)
- These CAN be the same directory if the user configures it that way
Trigger
User invokes /setup-project or /setup-project <project-name>
Steps
1. Validate Environment & Create Project Directory
-
Confirm $OUTWORKOS_ROOT is set and the template directory exists with these files:
$OUTWORKOS_ROOT/.mcp.json
$OUTWORKOS_ROOT/skills/context-map/
$OUTWORKOS_ROOT/skills/log/
$OUTWORKOS_ROOT/skills/todoist/
If any are missing, warn but continue (some may not be needed).
-
Confirm $OUTWORKOS_PARENT is set. If not, default to $OUTWORKOS_HOME/projects or ask the user.
-
Get the project name:
- If provided as argument, use it
- Otherwise, ask: "What's the name of this project?"
- Slugify the name for the directory (lowercase, hyphens for spaces, strip special chars)
-
Create the project directory at $OUTWORKOS_PARENT/<slug>/:
- If the directory already exists and is not empty, stop with an error: "Project directory already exists. Use a different name or remove it first."
- If the directory exists but is empty, proceed
- If it doesn't exist, create it with
mkdir -p
-
Set PROJECT_DIR=$OUTWORKOS_PARENT/<slug> for use in all subsequent steps.
Note: The user runs Claude from $OUTWORKOS_ROOT (the repo) to get skills loaded. This step creates the project directory elsewhere — that's expected.
2. Check for Existing Files
Check whether these already exist in the project directory:
.mcp.json
.claude/skills/context-map/
.claude/skills/log/
.claude/skills/todoist/
For each one that exists, ask the user whether to skip or overwrite it. Use a single AskUserQuestion with multiSelect for efficiency.
3. Verify Vault Access
Verify that outwork init has been run and Vault secrets are accessible. Test by retrieving a known secret:
"$OUTWORKOS_ROOT/scripts/get-secret.sh" todoist_api_token > /dev/null 2>&1 && echo "Vault: OK" || echo "Vault: FAILED — run outwork init first"
If Vault access fails, tell the user to run $OUTWORKOS_ROOT/scripts/outworkos-auth-login.sh first.
4. Link Skills and Hooks
Symlink key directories from $OUTWORKOS_ROOT into the project so skills, hooks, and environment variables work when running Claude from the project directory.
mkdir -p "$PROJECT_DIR/.claude"
ln -sf "$OUTWORKOS_ROOT/skills" "$PROJECT_DIR/.claude/commands"
ln -sf "$OUTWORKOS_ROOT/.claude/hooks" "$PROJECT_DIR/.claude/hooks"
if [ ! -f "$PROJECT_DIR/.claude/settings.json" ]; then
cp "$OUTWORKOS_ROOT/.claude/settings.json" "$PROJECT_DIR/.claude/settings.json"
fi
This ensures:
- All skills are available (context-map, log, todoist, scan, whats-next, etc.)
$OUTWORKOS_ROOT, $OUTWORKOS_HOME, $OUTWORKOS_PARENT are set on session start
- Vault scripts (
get-secret.sh, set-secret.sh) are reachable via $OUTWORKOS_ROOT/scripts/
- Stays in sync with the repo — no need to copy or update individual files
If .claude/commands or .claude/hooks already exists:
- If it's a symlink pointing to the same target, skip
- If it's a symlink pointing elsewhere, ask the user before replacing
- If it's a regular directory, ask the user whether to replace it with a symlink
5. Supabase Picker (Optional)
Ask the user: "Do you want to add Supabase database connections?"
If no: skip to Step 6 with zero Supabase entries.
If yes:
- Read
SUPABASE_ACCESS_TOKEN from Vault: $("$OUTWORKOS_ROOT/scripts/get-secret.sh" supabase_access_token)
- Call the Supabase Management API to list projects:
curl -s -H "Authorization: Bearer <TOKEN>" https://api.supabase.com/v1/projects
- Filter the JSON response to only projects where
status is "ACTIVE_HEALTHY"
- Present a numbered list showing each project's
name and ref (the project reference ID)
- Ask the user to select which projects to add (comma-separated numbers, e.g. "1,3,4")
- Store the selected projects for Step 6
6. Assemble .mcp.json
- Read the base
.mcp.json from $OUTWORKOS_ROOT/.mcp.json — this contains the 6 base servers (github, netlify, context7, perplexity, fireflies, google-workspace)
- Parse it as JSON
- For each Supabase project selected in Step 5, add an entry using the project's
name as the key:
"<project-name>": {
"command": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--project-ref",
"<project-ref>"
],
"env": {
"SUPABASE_ACCESS_TOKEN": "<token-value-from-env>"
}
}
- Write the assembled JSON to
.mcp.json in the current directory (pretty-printed with 2-space indent)
Important: Use the env field for the access token, NOT --access-token in args. This keeps secrets out of process arguments.
7. Test Connections
After assembling all files, test each API token/key by making a lightweight curl call. Read credentials from Vault via $OUTWORKOS_ROOT/scripts/get-secret.sh and from .mcp.json for MCP-specific tokens.
Run ALL test calls in parallel using separate Bash tool calls for speed.
Tests to run:
| Service | Test command | Pass | Fail action |
|---|
| GitHub | curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token <GITHUB_PERSONAL_ACCESS_TOKEN>" https://api.github.com/user | 200 | Token expired or invalid — regenerate at https://github.com/settings/tokens |
| Netlify | curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer <NETLIFY_AUTH_TOKEN>" https://api.netlify.com/api/v1/user | 200 | Token invalid — regenerate at https://app.netlify.com/user/applications#personal-access-tokens |
| Perplexity | curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer <PERPLEXITY_API_KEY>" -H "Content-Type: application/json" -d '{"model":"sonar","messages":[{"role":"user","content":"ping"}]}' https://api.perplexity.ai/chat/completions | 200 | API key invalid or expired — check https://www.perplexity.ai/settings/api |
| Supabase | curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer <SUPABASE_ACCESS_TOKEN>" https://api.supabase.com/v1/projects | 200 | PAT invalid — regenerate at https://supabase.com/dashboard/account/tokens |
| Fireflies | curl -s -o /dev/null -w "%{http_code}" -H "Content-Type: application/json" -H "Authorization: Bearer <FIREFLIES_TOKEN>" -d '{"query":"{ user { name } }"}' https://api.fireflies.ai/graphql | 200 | Token invalid — check https://app.fireflies.ai/integrations |
| Context7 | MCP call: mcp__context7__resolve-library-id (see below) | Returns results | API key invalid — check with Upstash |
How to extract each credential (use these exact methods):
-
Read .mcp.json with the Read tool and parse as JSON. Extract:
GITHUB_PERSONAL_ACCESS_TOKEN → mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN
NETLIFY_AUTH_TOKEN → mcpServers.netlify.env.NETLIFY_AUTH_TOKEN
PERPLEXITY_API_KEY → mcpServers.perplexity.env.PERPLEXITY_API_KEY
FIREFLIES_TOKEN → from mcpServers.fireflies.args, find the string that starts with "Authorization: Bearer " and extract everything after that prefix
-
For Supabase, extract from any Supabase entry in .mcp.json → mcpServers.<any-supabase-server>.env.SUPABASE_ACCESS_TOKEN. This is reliable because it's already a clean JSON string value. If no Supabase server was added, fall back to Vault: $("$OUTWORKOS_ROOT/scripts/get-secret.sh" supabase_access_token).
Important: Prefer reading from .mcp.json JSON values or Vault for all credential access.
Context7 test (MCP-based, not curl):
Context7 has no public REST endpoint, but the MCP server is typically already connected in the current session. Test it by making a lightweight MCP tool call:
- Use
ToolSearch to load mcp__context7__resolve-library-id
- Call it with
libraryName: "nextjs" and query: "test connection"
- If it returns library results → pass
- If it errors (auth failure, timeout) → fail — API key invalid, check with Upstash
Run this in parallel with the curl tests above (it's independent).
Services NOT tested (note in report but don't fail):
- Google Workspace: Uses OAuth refresh tokens — the workspace-mcp server handles token refresh internally. If the refresh token is expired, the server will prompt for re-auth on first use.
Handling failures:
For each failed test:
- Report which service failed and the HTTP status code received
- Show the specific URL where the user can regenerate the token
- After listing all failures, ask: "Would you like to update any of these tokens now?"
- If yes, for each token the user provides:
- Update the value in Vault via
set-secret.sh and/or .mcp.json as appropriate
- Re-run that specific test to confirm the new token works
- Repeat until the token passes or the user chooses to skip
- If all tests pass, continue to Step 8
8. Todoist Sections (if applicable)
If a Todoist project ID is already known for this project (e.g., from a previous /context-map run or from the user), ensure the standard status sections exist:
- Fetch sections via Sync API:
resource_types: ["sections"]
- Check if "Triage" section exists for the project ID
- If missing, create all four sections (Triage, Do, Waiting, Defer) in a single batch:
import json, uuid
sections = ["Triage", "Do", "Waiting", "Defer"]
commands = [{"type": "section_add", "uuid": str(uuid.uuid4()), "temp_id": str(uuid.uuid4()), "args": {"name": s, "project_id": PROJECT_ID}} for s in sections]
with open("/tmp/todoist_batch.json", "w") as f:
json.dump({"commands": commands}, f)
- If no Todoist project ID is known yet, skip —
/context-map will handle section creation when it sets up the Todoist project
Standard sections:
| Section | Purpose |
|---|
| Triage | New tasks, needs decision |
| Do | Committed, actively working |
| Waiting | Blocked on someone else |
| Defer | Parked for later |
9. GitHub Backup Repository (Optional)
Ask the user: "Do you want to set up a GitHub backup repository for this project?"
If no: skip to Step 10.
If yes:
9a. Prerequisites
- Verify
gh CLI is available: gh --version
- Verify
gh is authenticated: gh auth status
- If either fails, stop and tell the user to install/authenticate
gh
9b. Detect Existing Git State
- Check if
.git/ already exists in the project directory
- If yes, ask: "This directory already has a git repo. Skip GitHub backup setup?" — if they want to skip, go to Step 10
- Scan for subdirectories that contain their own
.git/ (app repos):
find . -mindepth 2 -maxdepth 3 -name ".git" -type d
- Record these paths — they'll be excluded via
.gitignore
9c. Generate .gitignore
Create .gitignore with:
# Local config
.mcp.json
# OS files
.DS_Store
Thumbs.db
# Dependencies and caches
node_modules/
__pycache__/
.cache/
*.pyc
# Large binary files
*.zip
*.tar.gz
*.dmg
For each subdirectory with its own .git/ detected in 9b, append a line:
# App repo (has its own git)
subdirectory-name/
9d. Initialize and Push
git init
- Derive repo name from the project directory name (lowercase, hyphens for spaces, strip special chars)
git add .
git commit -m "Initial project backup"
gh repo create MattVOLTA/<repo-name> --private --source=. --push
- Verify:
gh repo view MattVOLTA/<repo-name> --json url -q .url
9e. Create Backup Script
Write .claude/hooks/backup.sh:
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
[ -d .git ] || exit 0
if git diff --quiet HEAD 2>/dev/null && [ -z "$(git status --porcelain)" ]; then
exit 0
fi
git add -A
git commit -m "Auto-backup $(date +%Y-%m-%d\ %H:%M)" --no-gpg-sign 2>/dev/null
git push origin HEAD 2>/dev/null
Make it executable: chmod +x .claude/hooks/backup.sh
9f. Add SessionEnd Hook
Read the project's .claude/settings.json (create if missing). Add a SessionEnd hook entry:
{
"hooks": {
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/backup.sh\"",
"timeout": 30
}
]
}
]
}
}
If settings.json already has hooks (e.g., SessionStart), merge the new SessionEnd entry alongside existing hooks — do NOT overwrite.
9g. Verify
Confirm the repo exists and the hook file is in place:
gh repo view MattVOLTA/<repo-name> --json url
ls -la .claude/hooks/backup.sh
10. Report
Confirm all created files exist by reading them. Then print a summary:
Project setup complete!
Directory: $OUTWORKOS_PARENT/<project-name>/
Files created:
.mcp.json
.claude/commands → $OUTWORKOS_ROOT/skills/ (symlink — all skills available)
Connection tests:
✓ GitHub
✓ Supabase
· Google Workspace (OAuth — tested on first use)
...
MCP servers configured:
supabase, google-workspace, github, ...
GitHub backup:
✓ Repo: github.com/<user>/<repo-name> (private)
✓ SessionEnd hook: .claude/hooks/backup.sh
If GitHub backup was skipped, show:
GitHub backup:
· Skipped
Use ✓ for passed, ✗ for failed (with note), · for skipped/untestable.
Next step: Suggest: "Run cd $OUTWORKOS_PARENT/<project-name> && claude then /context-map to configure this project."