Project Add
Adds a project — create a new project or clone an existing GitHub repo — with symlinks (macOS) or junctions (Windows) to the shared claude-config.
Trigger
/project-add [name] or /project-add
Process
PHASE 0: Pre-flight Checks
Detect platform:
# Detect OS
case "$(uname -s)" in
Darwin) PLATFORM="macos" ;;
Linux) PLATFORM="linux" ;;
MINGW*|CYGWIN*|MSYS*) PLATFORM="windows" ;;
*) echo "Unsupported platform: $(uname -s)" >&2; exit 1 ;;
esac
Use the detected platform to resolve {projects_root} and {config_repo} from paths.yaml (see Configuration section below).
Before anything is created, validate:
# Fallback: if config_repo is missing but ~/.claude is already linked to a
# real claude-config, adopt that location instead of aborting (handles
# non-standard install locations).
if [ ! -d "$CONFIG_REPO" ] && [ -L "$HOME/.claude/skills" ]; then
CONFIG_REPO="$(dirname "$(readlink "$HOME/.claude/skills")")"
echo "config_repo not at default — resolved via ~/.claude/skills → $CONFIG_REPO"
fi
# Check claude-config exists and is complete
test -d "{config_repo}"
test -d "{config_repo}/scripts"
# Check gh CLI authenticated (needed for clone mode and publish)
gh auth status
If config check fails:
❌ claude-config not found or incomplete
Expected: {config_repo}
With folders: agents/, skills/, scripts/
Solution:
1. Clone claude-config repo to {config_repo}
2. Or set path via CLAUDE_CONFIG_REPO environment variable
→ Stop command, do NOT create any folders
If gh auth fails:
→ Store: GH_AVAILABLE=false. Show: gh not available — clone mode and GitHub publish skipped.
If checks pass: → Continue to PHASE 1
PHASE 1: Mode Selection
If name provided via /project-add [name]:
→ Assume: new project mode. Validate the name immediately (same rules as PHASE 2 (new): lowercase letters/digits/hyphens, no spaces or special characters, not existing in {projects_root}). On validation error: show the error and stop. If valid: store name and go to PHASE 3 (skip PHASE 2 (new) name question).
If no name provided:
If GH_AVAILABLE=false: show only "Create new project" (Clone requires gh).
question: "What do you want to do?"
header: "Mode"
options:
- label: "Create new project"
description: "Create an empty project with claude-config symlinks"
- label: "Clone existing repo" # only show if GH_AVAILABLE=true
description: "Clone a GitHub repo and configure claude-config symlinks"
multiSelect: false
→ New project: go to PHASE 2 (new) → Clone: go to PHASE 2 (clone)
PHASE 2 (new): Project Name
Ask name:
question: "What is the name of the new project?"
header: "Project"
options:
- label: "Type a name"
description: "Short, lowercase name without spaces (e.g. my-app)"
multiSelect: false
Validation:
- Lowercase letters, digits, hyphens
- No spaces or special characters
- Not existing in
{projects_root}
→ Go to PHASE 3
PHASE 2 (clone): Repo Selection
Two sub-options:
question: "How do you want to select the repo?"
header: "Repo"
options:
- label: "Browse my repos (Recommended)"
description: "Show list of your GitHub repos"
- label: "Enter manually"
description: "Type owner/repo or full GitHub URL"
multiSelect: false
Browse mode:
gh repo list --limit 30 --json name,description,isPrivate,url --jq '.[] | "\(.name)\t\(.description // "-")\t\(if .isPrivate then "🔒" else "🌐" end)\t\(.url)"'
Show as numbered list in plain text:
Available repos:
1. my-app — My cool app 🔒
2. website — Personal site 🌐
3. api-backend — REST API service 🔒
...
M. Load more
Q. Enter manually
Which repo? (number)
- User chooses number → select that repo
- M → load next 30 (
--limit 30with offset) - Q → switch to manual entry
Manual mode:
User types owner/repo or full GitHub URL (e.g. https://github.com/owner/repo).
Parse to owner/repo format.
After repo selection:
- Extract project name from repo name
- Check that
{projects_root}/[name]does not already exist - Clone:
gh repo clone <owner/repo> {projects_root}/[name]
→ Go to PHASE 3
PHASE 3: Setup Directories
Create project subdirectories (mkdir -p is safe for both modes):
mkdir -p {projects_root}/[name]/.claude/docs
mkdir -p {projects_root}/[name]/.claude/research
mkdir -p {projects_root}/[name]/.project/session
mkdir -p {projects_root}/[name]/.project/plans
mkdir -p {projects_root}/[name]/.project/features
New mode: creates everything from scratch.
Clone mode: project root already exists, only creates .claude/ and .project/ subdirs.
PHASE 4: Base Files
New mode:
Copy templates:
Write initial project files:
macOS / Linux:
cat > "{projects_root}/[name]/.project/project.json" << 'ENDJSON'
{
"schemaVersion": 2,
"seed": { "name": "[name]", "pitch": "", "content": "" },
"localUrl": "",
"theme": {
"colors": { "main": [], "accent": [], "semantic": [] },
"typography": { "families": { "heading": "", "body": "", "mono": "" }, "sizes": [] },
"spacing": { "base": "", "scale": [] },
"breakpoints": [],
"borderRadius": [],
"shadows": [],
"modes": {},
"cssVars": ""
},
"stack": { "framework": "", "language": "", "styling": "", "db": "", "auth": "", "hosting": "", "packages": [] },
"data": { "entities": [] },
"endpoints": [],
"team": { "mode": "solo" },
"thinking": []
}
ENDJSON
cat > "{projects_root}/[name]/.project/project-context.json" << 'ENDJSON'
{
"schemaVersion": 2,
"architecture": { "routes": [], "components": [], "endpoints": [], "entities": [], "diagram": "", "dataFlow": "" },
"context": { "structure": "", "routing": [], "patterns": [] },
"learnings": []
}
ENDJSON
Windows (PowerShell):
$projectJson = '{
"schemaVersion": 2,
"seed": { "name": "[name]", "pitch": "", "content": "" },
"localUrl": "",
"theme": {
"colors": { "main": [], "accent": [], "semantic": [] },
"typography": { "families": { "heading": "", "body": "", "mono": "" }, "sizes": [] },
"spacing": { "base": "", "scale": [] },
"breakpoints": [], "borderRadius": [], "shadows": [], "modes": {}, "cssVars": ""
},
"stack": { "framework": "", "language": "", "styling": "", "db": "", "auth": "", "hosting": "", "packages": [] },
"data": { "entities": [] },
"endpoints": [], "team": { "mode": "solo" }, "thinking": []
}'
Set-Content -Path "{projects_root}\[name]\.project\project.json" -Value $projectJson -Encoding UTF8
$ctxJson = '{
"schemaVersion": 2,
"architecture": { "routes": [], "components": [], "endpoints": [], "entities": [], "diagram": "", "dataFlow": "" },
"context": { "structure": "", "routing": [], "patterns": [] },
"learnings": []
}'
Set-Content -Path "{projects_root}\[name]\.project\project-context.json" -Value $ctxJson -Encoding UTF8
Replace [name] literally with the actual project name in both files.
# settings.local.json with default permissions
echo '{"permissions": {"allow": []}}' > {projects_root}/[name]/.claude/settings.local.json
.gitignore with standard content:
# Dependencies
node_modules/
# Build output
dist/
build/
# Environment
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
# OS
.DS_Store
Thumbs.db
# Claude tooling & local project data — not committed to the project repo
# (managed by the shared claude-config / master repo, not this one).
# Wholesale entries (no trailing slash) so symlinks, real dirs and sub-paths all match.
.claude/
.project/
CLAUDE.md
AGENTS.md
Clone mode:
Create settings.local.json:
echo '{"permissions": {"allow": []}}' > {projects_root}/[name]/.claude/settings.local.json
.gitignore — append claude-specific entries if not already present:
Check each entry below; append only the ones not already present (a wholesale entry like .claude/ already covers every sub-path, so skip any that are redundant). Use no trailing slash — .claude/agents is a symlink file, and a trailing-slash pattern (.claude/agents/) would only match a directory and silently fail to ignore it:
# Claude tooling & local project data — not committed to the project repo
# (managed by the shared claude-config / master repo, not this one).
.claude/
.project/
CLAUDE.md
AGENTS.md
If .gitignore does not exist, create it with the above entries.
PHASE 5: Config Symlinks
Link the shared claude-config into the project so skills/agents/hooks/scripts resolve at runtime. These four links are gitignored (PHASE 4) — per-device pointers, not tracked content. Runs for both new and clone mode.
macOS / Linux:
for d in agents hooks skills scripts; do
test -d "{config_repo}/$d" || { echo "WARN: {config_repo}/$d missing — skipping"; continue; }
ln -sfn "{config_repo}/$d" "{projects_root}/[name]/.claude/$d"
done
# Verify each link resolves
for d in agents hooks skills scripts; do
test -e "{projects_root}/[name]/.claude/$d" && echo "$d: OK" || echo "$d: BROKEN"
done
Windows (PowerShell — junctions, no admin needed):
foreach ($d in 'agents','hooks','skills','scripts') {
$target = "{config_repo}\$d"
if (!(Test-Path $target)) { Write-Warning "$target missing — skipping"; continue }
$link = "{projects_root}\[name]\.claude\$d"
if (Test-Path $link) { Remove-Item $link -Recurse -Force }
New-Item -ItemType Junction -Path $link -Target $target | Out-Null
}
PHASE 6: Git Initialization
New mode:
cd {projects_root}/[name]
git init
git add .gitignore
Clone mode:
Git init is skipped (repo is already initialized by gh repo clone). Instead, restore durable .project/ state if this project was synced from another device:
cd {projects_root}/[name]
git ls-remote --heads origin "claude/state*"
- A
claude/state*branch matches → followshared/STATE-SYNC.md § 9(Clone restore): discover the branch (§ 2 precedence), add a detached temp worktree at its tip, runstate-files.py restore, write.project/session/state-sync.json, remove the worktree. Script path:{config_repo}/skills/project-sync/scripts/state-files.py. Show:State restored: {N} files from {branch} ({shortsha}). - No match → skip silently (scaffold-only clone, unchanged behavior).
PHASE 7: Project Configuration
Write a setup marker so a later /core-setup run starts in the right mode. Do NOT run or offer to run /core-setup as part of this flow — it belongs in a fresh session, never chained into project-add.
Determine intended core-setup mode:
- New mode →
setup_mode = "greenfield" - Clone mode →
setup_mode = "mature"(cloned repo may already have source code)
Write marker (always — no prompt): set stateRestored to true only when PHASE 6 clone-mode restored state from a claude/state* branch, else false. A later /core-setup --mode=mature reads it: restored learnings/backlog already exist, so the mature scan runs additively (learnings dedup, team.mode skip-if-set) and its "pre-existing learnings" modal defaults to Continue.
mkdir -p .project/session
cat > ".project/session/setup-pending.json" << ENDJSON
{
"source": "project-add",
"mode": "{setup_mode}",
"stateRestored": {state_restored},
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
ENDJSON
Show: Setup marker written — a later /core-setup run starts directly in {setup_mode} mode.
PHASE 8: GitHub Publish
New mode:
If GH_AVAILABLE=false: skip this phase. Show: GitHub publish skipped — gh not authenticated. Go to PHASE 9.
question: "Do you want to publish the repo to GitHub?"
header: "Publish"
options:
- label: "Yes, create private repo (Recommended)"
description: "Publish as private GitHub repository"
- label: "Yes, create public repo"
description: "Publish as public GitHub repository"
- label: "No, later"
description: "Skip, publish manually later"
multiSelect: false
If publish desired:
- Stage all files and create initial commit:
cd {projects_root}/[name]
git add -A
git commit -m "feat: initial commit - [name]"
- Ask short description (optional — free text):
Show: Short GitHub description (optional, Enter to skip):
Read user input → store as REPO_DESC (can be empty).
- Create GitHub repo and push:
# Build description argument as bash array (empty = no flag)
if [ -n "$REPO_DESC" ]; then
DESC_FLAG=(--description "$REPO_DESC")
else
DESC_FLAG=()
fi
# Private repo
gh repo create [name] --private --source=. --push "${DESC_FLAG[@]}"
# OR public repo
gh repo create [name] --public --source=. --push "${DESC_FLAG[@]}"
- Show repo URL after successful publish
Requirements for publish:
ghCLI installed and authenticated- Check with
gh auth statusbefore starting
Clone mode:
→ Skip (repo is already on GitHub)
Show: GitHub: [repo URL]
PHASE 9: Shell Alias
Ask:
question: "Do you want to create a shell alias to quickly open this project?"
header: "Alias"
options:
- label: "Yes, create alias"
description: "Add alias to ~/.bashrc that runs cd + claude"
- label: "No, skip"
description: "Don't create alias"
multiSelect: false
If alias desired:
Suggest a short alias based on the project name (first letters, abbreviation, or initials). Let the user confirm or change it.
question: "Which alias do you want to use?"
header: "Alias"
options:
- label: "[suggestion] (Recommended)"
description: "alias [suggestion]='cd {projects_root}/[name] && claude'"
- label: "Different name"
description: "Type your own alias name"
multiSelect: false
Validation:
- Alias must not already exist in the target rc-file
- Lowercase letters only, max 4 characters (short and fast)
Add:
Detect shell and choose rc-file:
case "$SHELL" in
*/zsh) RC_FILE="$HOME/.zshrc" ;;
*/bash) RC_FILE="$HOME/.bashrc" ;;
*/fish) RC_FILE="$HOME/.config/fish/config.fish" ;;
*) RC_FILE="$HOME/.profile" ;;
esac
echo "alias [alias]='cd {projects_root}/[name] && claude'" >> "$RC_FILE"
# Reload the rc-file so the alias is registered (errors suppressed —
# interactive-only rc lines may warn in a non-interactive shell)
source "$RC_FILE" 2>/dev/null || true
Note:
sourceruns in the skill's subprocess, so the alias is not active in the user's current terminal (a separate process). It only takes effect in a new terminal or after the user runssource $RC_FILEthemselves — reflect that in the confirm message.
Confirm:
Alias created: [alias] → cd {projects_root}/[name] && claude
Added to: $RC_FILE
Active in new terminals — for this terminal, run: source $RC_FILE
PHASE 10: Wrap Up
Output (new mode):
✅ Project [name] created
Structure:
{projects_root}/[name]/
├── .claude/
│ ├── docs/
│ ├── research/
│ └── CLAUDE.md (or yet to configure)
├── .project/
└── .gitignore
Alias: [alias] → cd {projects_root}/[name] && claude (if created)
GitHub: https://github.com/[user]/[name] (if published)
Output (clone mode):
✅ Project [name] cloned and configured
Source: https://github.com/[owner]/[repo]
Structure:
{projects_root}/[name]/
├── .claude/
│ ├── docs/
│ ├── research/
│ └── CLAUDE.md (or yet to configure)
├── .project/
├── .gitignore (updated with claude entries)
└── [existing repo files]
Alias: [alias] → cd {projects_root}/[name] && claude (if created)
GitHub: https://github.com/[owner]/[repo]
Configuration
Paths are configurable per device. Defaults are platform-dependent:
| Placeholder | macOS Default | Windows Default | Environment Variable |
|---|---|---|---|
{projects_root} |
$HOME/projects |
C:\Projects |
CLAUDE_PROJECTS_ROOT |
{config_repo} |
$HOME/claude-config |
C:\Projects\claude-config |
CLAUDE_CONFIG_REPO |
Resolution order (first match wins):
- Environment variable
.claude/paths.local.yaml(local per project, not in git)paths.yaml(skill root — shared defaults, platform section)
Restrictions
- Supported on macOS/Linux (symlinks) and Windows (junctions)
- Project name must be unique in
{projects_root} - Master config must exist in
{config_repo} - Clone mode requires
ghCLI authenticated - GitHub publish requires
ghCLI authenticated