# Project Add

> Add or clone a project into the multi-project setup. Use with /project-add.

- Skill: `airmile/project-add` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add airmile/project-add`
- Raw SKILL.md: https://api.skillmd.com/api/skills/airmile/project-add/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: AirMile (https://skillmd.com/u/airmile)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/airmile/project-add

---


# 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:**

```bash
# 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:**

```bash
# 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).

```yaml
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:**

```yaml
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:**

```yaml
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:

```bash
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 30` with 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:**

1. Extract project name from repo name
2. Check that `{projects_root}/[name]` does not already exist
3. Clone:

```bash
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):**

```bash
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:

```bash
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):

```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.**

```bash
# 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:**

```bash
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:

```bash
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):

```powershell
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:

```bash
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:

```bash
cd {projects_root}/[name]
git ls-remote --heads origin "claude/state*"
```

- **A `claude/state*` branch matches** → follow `shared/STATE-SYNC.md § 9` (Clone restore): discover the branch (§ 2 precedence), add a detached temp worktree at its tip, run `state-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.

```bash
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.

```yaml
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:**

1. Stage all files and create initial commit:

```bash
cd {projects_root}/[name]
git add -A
git commit -m "feat: initial commit - [name]"
```

2. **Ask short description** (optional — free text):

Show: `Short GitHub description (optional, Enter to skip):`
Read user input → store as `REPO_DESC` (can be empty).

3. Create GitHub repo and push:

```bash
# 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[@]}"
```

4. Show repo URL after successful publish

**Requirements for publish:**

- `gh` CLI installed and authenticated
- Check with `gh auth status` before starting

#### Clone mode:

→ Skip (repo is already on GitHub)

Show: `GitHub: [repo URL]`

### PHASE 9: Shell Alias

**Ask:**

```yaml
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.

```yaml
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:

```bash
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:** `source` runs 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 runs `source $RC_FILE` themselves — 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):**

1. Environment variable
2. `.claude/paths.local.yaml` (local per project, not in git)
3. `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 `gh` CLI authenticated
- GitHub publish requires `gh` CLI authenticated

