Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions Resources/crow-workspace-session-settings.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"permissions": {
"allow": [
"Bash(gh issue view:*)",
"Bash(gh issue list:*)",
"Bash(gh issue edit:*)",
"Bash(gh pr view:*)",
"Bash(gh pr list:*)",
"Bash(gh pr diff:*)",
"Bash(gh pr checks:*)",
"Bash(gh pr create:*)",
"Bash(gh pr edit:*)",
"Bash(gh pr review:*)",
"Bash(gh pr comment:*)",
"Bash(gh repo view:*)",
"Bash(gh api graphql:*)",
"Bash(gh api repos:*)",
"Bash(gh auth status:*)",
"Bash(gh search:*)",

"Bash(GITLAB_HOST=* glab issue view:*)",
"Bash(GITLAB_HOST=* glab mr view:*)",
"Bash(GITLAB_HOST=* glab mr list:*)",
"Bash(GITLAB_HOST=* glab mr create:*)",
"Bash(GITLAB_HOST=* glab auth status:*)",
"Bash(glab issue view:*)",
"Bash(glab mr view:*)",
"Bash(glab mr list:*)",

"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git fetch:*)",
"Bash(git pull:*)",
"Bash(git push:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git checkout:*)",
"Bash(git branch:*)",
"Bash(git stash:*)",
"Bash(git rebase:*)",
"Bash(git merge:*)",
"Bash(git -C:*)",
"Bash(git ls-remote:*)",
"Bash(git worktree:*)",

"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(which:*)",
"Bash(sleep:*)",
"Bash(mkdir -p:*)",
"Bash(tee:*)",
"Bash(wc:*)",
"Bash(jq:*)",

"Bash(* > $TMPDIR/*)",
"Bash(* > /tmp/claude/*)",
"Bash(* | tee $TMPDIR/*)",
"Bash(* | tee /tmp/claude/*)",

"Bash(crow get-session:*)",
"Bash(crow list-worktrees:*)",
"Bash(crow list-links:*)",
"Bash(crow set-status:*)",

"Read",
"Write",
"Edit",
"Glob",
"Grep"
]
},
"sandbox": {
"exclude_commands": ["git", "crow", "gh", "glab"]
}
}
28 changes: 28 additions & 0 deletions Sources/Crow/App/Scaffolder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ struct Scaffolder {
try setupScript.write(toFile: setupScriptPath, atomically: true, encoding: .utf8)
try fm.setAttributes([.posixPermissions: 0o755], ofItemAtPath: setupScriptPath)

// Always overwrite the baseline session-settings template (used by setup.sh
// to seed each new worktree with a `.claude/settings.local.json`)
let sessionSettingsPath = (skillsDir as NSString).appendingPathComponent("session-settings.template.json")
let sessionSettingsTemplate = Self.bundledSessionSettings()
try sessionSettingsTemplate.write(toFile: sessionSettingsPath, atomically: true, encoding: .utf8)

// Always overwrite the review-pr skill with the latest version
let reviewSkillPath = (reviewSkillsDir as NSString).appendingPathComponent("SKILL.md")
let reviewSkillTemplate = Self.bundledReviewSkill()
Expand Down Expand Up @@ -151,6 +157,28 @@ struct Scaffolder {
"""
}

/// The crow-workspace baseline session settings template bundled with the app.
/// Dropped next to setup.sh so each new worktree can seed
/// `.claude/settings.local.json` from it.
static func bundledSessionSettings() -> String {
if let content = loadFromRepo("skills/crow-workspace/session-settings.template.json") {
return content
}
if let url = Bundle.main.url(forResource: "crow-workspace-session-settings.json", withExtension: "template"),
let content = try? String(contentsOf: url) {
return content
}
// Minimal fallback — keeps setup.sh's cp from failing if the template
// ever goes missing. Empty allow list = no extra permissions granted.
return """
{
"permissions": {
"allow": []
}
}
"""
}

/// The crow-review-pr SKILL.md template bundled with the app.
static func bundledReviewSkill() -> String {
if let content = loadFromRepo("skills/crow-review-pr/SKILL.md") {
Expand Down
10 changes: 10 additions & 0 deletions skills/crow-workspace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,16 @@ For cross-workspace setups with multiple repos, call `setup.sh` once per repo:
--skip-launch
```

### Baseline Permissions

`setup.sh` writes a baseline `.claude/settings.local.json` into each new worktree before launching Claude Code, sourced from `session-settings.template.json` next to the script. The file is gitignored by Claude Code on creation, and its `permissions.allow` array merges with any `.claude/settings.json` shipped by the repo — so it augments rather than overrides the project's own allowlist.

The baseline covers commands the prompt template instructs sessions to run on step 1 (`gh issue view`, `glab issue view`, `gh pr view`), the git operations a feature branch needs (`status`, `diff`, `add`, `commit`, `push`, …), common shell utilities (`cat`, `grep`, `jq`, `tee`), and redirects to `$TMPDIR` / `/tmp/claude`.

The template also sets `sandbox.exclude_commands: [git, crow, gh, glab]` so users who have the sandbox enabled don't get an "unsandboxed" prompt for these commands. Crucially the template does **not** set `sandbox.enabled` — that key takes the highest-precedence value across scopes, so omitting it keeps each user's existing sandbox preference (off stays off, on stays on); the exclude list is a no-op when sandbox is disabled.

If `{worktree}/.claude/settings.local.json` already exists (e.g. checked in by the repo, or left over from a previous setup run), the script leaves it untouched. Edit `session-settings.template.json` to extend the baseline.

## First Prompt Template

IMPORTANT: Always use full absolute paths, never abbreviated (`...`) or home-relative (`~`) paths.
Expand Down
82 changes: 82 additions & 0 deletions skills/crow-workspace/session-settings.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"permissions": {
"allow": [
"Bash(gh issue view:*)",
"Bash(gh issue list:*)",
"Bash(gh issue edit:*)",
"Bash(gh pr view:*)",
"Bash(gh pr list:*)",
"Bash(gh pr diff:*)",
"Bash(gh pr checks:*)",
"Bash(gh pr create:*)",
"Bash(gh pr edit:*)",
"Bash(gh pr review:*)",
"Bash(gh pr comment:*)",
"Bash(gh repo view:*)",
"Bash(gh api graphql:*)",
"Bash(gh api repos:*)",
"Bash(gh auth status:*)",
"Bash(gh search:*)",

"Bash(GITLAB_HOST=* glab issue view:*)",
"Bash(GITLAB_HOST=* glab mr view:*)",
"Bash(GITLAB_HOST=* glab mr list:*)",
"Bash(GITLAB_HOST=* glab mr create:*)",
"Bash(GITLAB_HOST=* glab auth status:*)",
"Bash(glab issue view:*)",
"Bash(glab mr view:*)",
"Bash(glab mr list:*)",

"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git fetch:*)",
"Bash(git pull:*)",
"Bash(git push:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git checkout:*)",
"Bash(git branch:*)",
"Bash(git stash:*)",
"Bash(git rebase:*)",
"Bash(git merge:*)",
"Bash(git -C:*)",
"Bash(git ls-remote:*)",
"Bash(git worktree:*)",

"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(which:*)",
"Bash(sleep:*)",
"Bash(mkdir -p:*)",
"Bash(tee:*)",
"Bash(wc:*)",
"Bash(jq:*)",

"Bash(* > $TMPDIR/*)",
"Bash(* > /tmp/claude/*)",
"Bash(* | tee $TMPDIR/*)",
"Bash(* | tee /tmp/claude/*)",

"Bash(crow get-session:*)",
"Bash(crow list-worktrees:*)",
"Bash(crow list-links:*)",
"Bash(crow set-status:*)",

"Read",
"Write",
"Edit",
"Glob",
"Grep"
]
},
"sandbox": {
"exclude_commands": ["git", "crow", "gh", "glab"]
}
}
42 changes: 42 additions & 0 deletions skills/crow-workspace/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ set -uo pipefail
# handled explicitly with `if ! ...` or `|| die/log` so that we always
# emit structured JSON on failure instead of silently exiting.

# ─── Script Location ─────────────────────────────────────────────────────────

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# ─── Defaults ────────────────────────────────────────────────────────────────

DEV_ROOT=""
Expand Down Expand Up @@ -245,6 +249,43 @@ setup_worktree() {
log "Worktree created successfully"
}

# ─── Baseline Allowlist ──────────────────────────────────────────────────────

# Drop a baseline `.claude/settings.local.json` into the new worktree so the
# launched Claude Code session doesn't prompt for the routine `gh issue view`,
# `git`, `cat`, `tee`, … commands the prompt template tells it to run.
#
# Source of truth is `session-settings.template.json` next to this script.
# Best-effort: failures are logged, not fatal — a missing template should never
# block worktree setup. Skips if the file already exists (e.g. checked in by
# the repo, or left over from a previous run).
write_session_settings() {
local settings_dir="$WORKTREE_PATH/.claude"
local settings_path="$settings_dir/settings.local.json"
local template_path="$SCRIPT_DIR/session-settings.template.json"

if [[ ! -f "$template_path" ]]; then
log "Baseline allowlist template not found at $template_path, skipping"
return
fi

if ! mkdir -p "$settings_dir" 2>/dev/null; then
log "Warning: could not create $settings_dir, skipping baseline allowlist"
return
fi

if [[ -f "$settings_path" ]]; then
log "$settings_path already exists, leaving untouched"
return
fi

if cp "$template_path" "$settings_path" 2>/dev/null; then
log "Wrote baseline allowlist to $settings_path"
else
log "Warning: failed to copy baseline allowlist to $settings_path"
fi
}

# ─── Crow Session ────────────────────────────────────────────────────────────

create_session() {
Expand Down Expand Up @@ -526,6 +567,7 @@ main() {
preflight

setup_worktree
write_session_settings
create_session
github_ops
write_prompt
Expand Down
Loading