mirror of
https://github.com/luongnv89/claude-howto.git
synced 2026-06-01 10:31:33 +02:00
refactor(advanced-features): narrow auto-mode permissions baseline
- Replace broad wildcard allowlist with conservative defaults - Add opt-in flags for edits, tests, git writes, packages, and gh writes - Update README to match the new permission tiers
This commit is contained in:
@@ -425,7 +425,7 @@ This ensures the user always retains control when the classifier cannot confiden
|
|||||||
|
|
||||||
### Seeding Auto-Mode-Equivalent Permissions (No Team Plan Required)
|
### Seeding Auto-Mode-Equivalent Permissions (No Team Plan Required)
|
||||||
|
|
||||||
If you don't have a Team plan or want a simpler approach without the background classifier, you can seed your `~/.claude/settings.json` with ~67 safe permission rules that cover the same ground as auto mode's default allow-list.
|
If you don't have a Team plan or want a simpler approach without the background classifier, you can seed your `~/.claude/settings.json` with a conservative baseline of safe permission rules. The script starts with read-only and local-inspection rules, then lets you opt into edits, tests, local git writes, package installs, and GitHub write actions only when you want them.
|
||||||
|
|
||||||
**File:** `09-advanced-features/setup-auto-mode-permissions.py`
|
**File:** `09-advanced-features/setup-auto-mode-permissions.py`
|
||||||
|
|
||||||
@@ -433,16 +433,23 @@ If you don't have a Team plan or want a simpler approach without the background
|
|||||||
# Preview what would be added (no changes written)
|
# Preview what would be added (no changes written)
|
||||||
python3 09-advanced-features/setup-auto-mode-permissions.py --dry-run
|
python3 09-advanced-features/setup-auto-mode-permissions.py --dry-run
|
||||||
|
|
||||||
# Apply once — safe to re-run (skips rules already present)
|
# Apply the conservative baseline
|
||||||
python3 09-advanced-features/setup-auto-mode-permissions.py
|
python3 09-advanced-features/setup-auto-mode-permissions.py
|
||||||
|
|
||||||
|
# Add more capability only when you need it
|
||||||
|
python3 09-advanced-features/setup-auto-mode-permissions.py --include-edits --include-tests
|
||||||
|
python3 09-advanced-features/setup-auto-mode-permissions.py --include-git-write --include-packages
|
||||||
```
|
```
|
||||||
|
|
||||||
The script adds rules across these categories:
|
The script adds rules across these categories:
|
||||||
|
|
||||||
| Category | Examples |
|
| Category | Examples |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
| Built-in tools | `Read(*)`, `Edit(*)`, `Write(*)`, `Glob(*)`, `Grep(*)`, `Agent(*)`, `WebSearch(*)` |
|
| Core read-only tools | `Read(*)`, `Glob(*)`, `Grep(*)`, `Agent(*)`, `WebSearch(*)`, `WebFetch(*)` |
|
||||||
| Git (read) | `Bash(git status:*)`, `Bash(git log:*)`, `Bash(git diff:*)` |
|
| Local inspection | `Bash(git status:*)`, `Bash(git log:*)`, `Bash(git diff:*)`, `Bash(cat:*)` |
|
||||||
|
| Optional edits | `Edit(*)`, `Write(*)`, `NotebookEdit(*)` |
|
||||||
|
| Optional test/build | `Bash(pytest:*)`, `Bash(python3 -m pytest:*)`, `Bash(cargo test:*)` |
|
||||||
|
| Optional git writes | `Bash(git add:*)`, `Bash(git commit:*)`, `Bash(git stash:*)` |
|
||||||
| Git (local write) | `Bash(git add:*)`, `Bash(git commit:*)`, `Bash(git checkout:*)` |
|
| Git (local write) | `Bash(git add:*)`, `Bash(git commit:*)`, `Bash(git checkout:*)` |
|
||||||
| Package managers | `Bash(npm install:*)`, `Bash(pip install:*)`, `Bash(cargo build:*)` |
|
| Package managers | `Bash(npm install:*)`, `Bash(pip install:*)`, `Bash(cargo build:*)` |
|
||||||
| Build & test | `Bash(make:*)`, `Bash(pytest:*)`, `Bash(go test:*)` |
|
| Build & test | `Bash(make:*)`, `Bash(pytest:*)`, `Bash(go test:*)` |
|
||||||
|
|||||||
@@ -2,82 +2,36 @@
|
|||||||
"""
|
"""
|
||||||
setup-auto-mode-permissions.py
|
setup-auto-mode-permissions.py
|
||||||
|
|
||||||
One-time script to seed ~/.claude/settings.json with ~67 safe permission rules
|
Seed ~/.claude/settings.json with a conservative baseline of safe permissions
|
||||||
equivalent to Claude Code's auto-mode baseline. Run once; idempotent (safe to
|
for Claude Code. The default set is read-only and local-inspection oriented;
|
||||||
re-run — skips rules already present).
|
optional flags let you widen the allowlist for editing, test execution, git
|
||||||
|
write operations, package installs, and GitHub CLI writes.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 setup-auto-mode-permissions.py
|
python3 setup-auto-mode-permissions.py
|
||||||
python3 setup-auto-mode-permissions.py --dry-run # preview without writing
|
python3 setup-auto-mode-permissions.py --dry-run
|
||||||
|
python3 setup-auto-mode-permissions.py --include-edits --include-tests
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
SETTINGS_PATH = Path.home() / ".claude" / "settings.json"
|
SETTINGS_PATH = Path.home() / ".claude" / "settings.json"
|
||||||
|
|
||||||
# ~67 safe, local, reversible permission rules (auto-mode equivalent)
|
# Core baseline: read-only inspection and low-risk local shell commands.
|
||||||
SAFE_PERMISSIONS = [
|
CORE_PERMISSIONS = [
|
||||||
# ── Built-in Claude Code tools ────────────────────────────────────────────
|
|
||||||
"Read(*)",
|
"Read(*)",
|
||||||
"Edit(*)",
|
|
||||||
"Write(*)",
|
|
||||||
"Glob(*)",
|
"Glob(*)",
|
||||||
"Grep(*)",
|
"Grep(*)",
|
||||||
"Agent(*)",
|
"Agent(*)",
|
||||||
"Skill(*)",
|
"Skill(*)",
|
||||||
"WebSearch(*)",
|
"WebSearch(*)",
|
||||||
"WebFetch(*)",
|
"WebFetch(*)",
|
||||||
"NotebookEdit(*)",
|
|
||||||
"TaskCreate(*)",
|
|
||||||
"TaskUpdate(*)",
|
|
||||||
|
|
||||||
# ── Git read-only ─────────────────────────────────────────────────────────
|
|
||||||
"Bash(git status:*)",
|
|
||||||
"Bash(git log:*)",
|
|
||||||
"Bash(git diff:*)",
|
|
||||||
"Bash(git branch:*)",
|
|
||||||
"Bash(git show:*)",
|
|
||||||
"Bash(git rev-parse:*)",
|
|
||||||
"Bash(git remote -v:*)",
|
|
||||||
"Bash(git remote get-url:*)",
|
|
||||||
"Bash(git stash list:*)",
|
|
||||||
"Bash(git fetch:*)",
|
|
||||||
|
|
||||||
# ── Git write (local, reversible) ─────────────────────────────────────────
|
|
||||||
"Bash(git add:*)",
|
|
||||||
"Bash(git commit:*)",
|
|
||||||
"Bash(git checkout:*)",
|
|
||||||
"Bash(git switch:*)",
|
|
||||||
"Bash(git merge:*)",
|
|
||||||
"Bash(git rebase:*)",
|
|
||||||
"Bash(git stash:*)",
|
|
||||||
"Bash(git tag:*)",
|
|
||||||
"Bash(git worktree:*)",
|
|
||||||
|
|
||||||
# ── Package managers ──────────────────────────────────────────────────────
|
|
||||||
"Bash(npm install:*)",
|
|
||||||
"Bash(npm ci:*)",
|
|
||||||
"Bash(npm test:*)",
|
|
||||||
"Bash(npm run:*)",
|
|
||||||
"Bash(npm audit:*)",
|
|
||||||
"Bash(npx:*)",
|
|
||||||
"Bash(pip install:*)",
|
|
||||||
"Bash(pip3 install:*)",
|
|
||||||
"Bash(cargo build:*)",
|
|
||||||
"Bash(cargo test:*)",
|
|
||||||
"Bash(go build:*)",
|
|
||||||
"Bash(go test:*)",
|
|
||||||
"Bash(go mod:*)",
|
|
||||||
|
|
||||||
# ── Build & test ──────────────────────────────────────────────────────────
|
|
||||||
"Bash(make:*)",
|
|
||||||
"Bash(cmake:*)",
|
|
||||||
"Bash(pytest:*)",
|
|
||||||
"Bash(python3 -m pytest:*)",
|
|
||||||
|
|
||||||
# ── Common safe shell commands ────────────────────────────────────────────
|
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(pwd:*)",
|
"Bash(pwd:*)",
|
||||||
"Bash(which:*)",
|
"Bash(which:*)",
|
||||||
@@ -92,64 +46,218 @@ SAFE_PERMISSIONS = [
|
|||||||
"Bash(dirname:*)",
|
"Bash(dirname:*)",
|
||||||
"Bash(basename:*)",
|
"Bash(basename:*)",
|
||||||
"Bash(realpath:*)",
|
"Bash(realpath:*)",
|
||||||
"Bash(mkdir:*)",
|
|
||||||
"Bash(touch:*)",
|
|
||||||
"Bash(cp:*)",
|
|
||||||
"Bash(mv:*)",
|
|
||||||
"Bash(chmod:*)",
|
|
||||||
"Bash(date:*)",
|
|
||||||
"Bash(env:*)",
|
|
||||||
"Bash(printenv:*)",
|
|
||||||
"Bash(file:*)",
|
"Bash(file:*)",
|
||||||
"Bash(stat:*)",
|
"Bash(stat:*)",
|
||||||
"Bash(diff:*)",
|
"Bash(diff:*)",
|
||||||
"Bash(md5sum:*)",
|
"Bash(md5sum:*)",
|
||||||
"Bash(sha256sum:*)",
|
"Bash(sha256sum:*)",
|
||||||
|
"Bash(date:*)",
|
||||||
|
"Bash(env:*)",
|
||||||
|
"Bash(printenv:*)",
|
||||||
|
"Bash(git status:*)",
|
||||||
|
"Bash(git log:*)",
|
||||||
|
"Bash(git diff:*)",
|
||||||
|
"Bash(git branch:*)",
|
||||||
|
"Bash(git show:*)",
|
||||||
|
"Bash(git rev-parse:*)",
|
||||||
|
"Bash(git remote -v:*)",
|
||||||
|
"Bash(git remote get-url:*)",
|
||||||
|
"Bash(git stash list:*)",
|
||||||
|
]
|
||||||
|
|
||||||
# ── GitHub CLI (read & common write) ──────────────────────────────────────
|
# Optional but still local: file edits and task bookkeeping.
|
||||||
|
EDITING_PERMISSIONS = [
|
||||||
|
"Edit(*)",
|
||||||
|
"Write(*)",
|
||||||
|
"NotebookEdit(*)",
|
||||||
|
"TaskCreate(*)",
|
||||||
|
"TaskUpdate(*)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional dev/test commands. These can still execute arbitrary project scripts,
|
||||||
|
# so keep them opt-in rather than part of the default baseline.
|
||||||
|
TEST_AND_BUILD_PERMISSIONS = [
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(cargo test:*)",
|
||||||
|
"Bash(go test:*)",
|
||||||
|
"Bash(pytest:*)",
|
||||||
|
"Bash(python3 -m pytest:*)",
|
||||||
|
"Bash(make:*)",
|
||||||
|
"Bash(cmake:*)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional local git write operations. History-rewriting commands stay out of
|
||||||
|
# the default baseline because they are easy to misuse.
|
||||||
|
GIT_WRITE_PERMISSIONS = [
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git checkout:*)",
|
||||||
|
"Bash(git switch:*)",
|
||||||
|
"Bash(git stash:*)",
|
||||||
|
"Bash(git tag:*)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional dependency/package commands. These are intentionally excluded from
|
||||||
|
# the default baseline because they can execute project hooks or fetch code.
|
||||||
|
PACKAGE_MANAGER_PERMISSIONS = [
|
||||||
|
"Bash(npm ci:*)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(pip install:*)",
|
||||||
|
"Bash(pip3 install:*)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional GitHub CLI write access.
|
||||||
|
GITHUB_WRITE_PERMISSIONS = [
|
||||||
|
"Bash(gh pr create:*)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional extra GitHub CLI read access.
|
||||||
|
GITHUB_READ_PERMISSIONS = [
|
||||||
"Bash(gh pr view:*)",
|
"Bash(gh pr view:*)",
|
||||||
"Bash(gh pr list:*)",
|
"Bash(gh pr list:*)",
|
||||||
"Bash(gh pr create:*)",
|
|
||||||
"Bash(gh issue view:*)",
|
"Bash(gh issue view:*)",
|
||||||
"Bash(gh issue list:*)",
|
"Bash(gh issue list:*)",
|
||||||
"Bash(gh repo view:*)",
|
"Bash(gh repo view:*)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def parse_args() -> argparse.Namespace:
|
||||||
dry_run = "--dry-run" in sys.argv
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Seed Claude Code settings with a conservative permission baseline."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Preview the rules that would be added without writing settings.json",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-edits",
|
||||||
|
action="store_true",
|
||||||
|
help="Add file-editing permissions (Edit/Write/NotebookEdit/TaskCreate/TaskUpdate)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-tests",
|
||||||
|
action="store_true",
|
||||||
|
help="Add local build/test commands such as pytest, cargo test, and make",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-git-write",
|
||||||
|
action="store_true",
|
||||||
|
help="Add local git mutation commands such as add, commit, checkout, and stash",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-packages",
|
||||||
|
action="store_true",
|
||||||
|
help="Add package install commands such as npm ci, npm install, and pip install",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-gh-write",
|
||||||
|
action="store_true",
|
||||||
|
help="Add GitHub CLI write permissions such as gh pr create",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-gh-read",
|
||||||
|
action="store_true",
|
||||||
|
help="Add GitHub CLI read permissions such as gh pr view and gh repo view",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
# Load existing settings (or start fresh)
|
|
||||||
if SETTINGS_PATH.exists():
|
def load_settings(path: Path) -> dict:
|
||||||
with open(SETTINGS_PATH) as f:
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with path.open() as f:
|
||||||
settings = json.load(f)
|
settings = json.load(f)
|
||||||
else:
|
except json.JSONDecodeError as exc:
|
||||||
settings = {}
|
raise SystemExit(f"Invalid JSON in {path}: {exc}") from exc
|
||||||
|
|
||||||
|
if not isinstance(settings, dict):
|
||||||
|
raise SystemExit(f"Expected {path} to contain a JSON object.")
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
def build_permissions(args: argparse.Namespace) -> list[str]:
|
||||||
|
permissions = list(CORE_PERMISSIONS)
|
||||||
|
|
||||||
|
if args.include_edits:
|
||||||
|
permissions.extend(EDITING_PERMISSIONS)
|
||||||
|
|
||||||
|
if args.include_tests:
|
||||||
|
permissions.extend(TEST_AND_BUILD_PERMISSIONS)
|
||||||
|
|
||||||
|
if args.include_git_write:
|
||||||
|
permissions.extend(GIT_WRITE_PERMISSIONS)
|
||||||
|
|
||||||
|
if args.include_packages:
|
||||||
|
permissions.extend(PACKAGE_MANAGER_PERMISSIONS)
|
||||||
|
|
||||||
|
if args.include_gh_write:
|
||||||
|
permissions.extend(GITHUB_WRITE_PERMISSIONS)
|
||||||
|
|
||||||
|
if args.include_gh_read:
|
||||||
|
permissions.extend(GITHUB_READ_PERMISSIONS)
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
|
||||||
|
def append_unique(existing: list, new_items: Iterable[str]) -> list[str]:
|
||||||
|
seen = set(existing)
|
||||||
|
added: list[str] = []
|
||||||
|
for item in new_items:
|
||||||
|
if item not in seen:
|
||||||
|
existing.append(item)
|
||||||
|
seen.add(item)
|
||||||
|
added.append(item)
|
||||||
|
return added
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_write_json(path: Path, payload: dict) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
dir=str(path.parent),
|
||||||
|
delete=False,
|
||||||
|
) as tmp:
|
||||||
|
json.dump(payload, tmp, indent=2)
|
||||||
|
tmp.write("\n")
|
||||||
|
tmp_path = Path(tmp.name)
|
||||||
|
|
||||||
|
tmp_path.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
permissions_to_add = build_permissions(args)
|
||||||
|
|
||||||
|
settings = load_settings(SETTINGS_PATH)
|
||||||
permissions = settings.setdefault("permissions", {})
|
permissions = settings.setdefault("permissions", {})
|
||||||
allow = permissions.setdefault("allow", [])
|
|
||||||
existing = set(allow)
|
|
||||||
|
|
||||||
added = [r for r in SAFE_PERMISSIONS if r not in existing]
|
if not isinstance(permissions, dict):
|
||||||
|
raise SystemExit("Expected permissions to be a JSON object.")
|
||||||
|
|
||||||
|
allow = permissions.setdefault("allow", [])
|
||||||
|
if not isinstance(allow, list):
|
||||||
|
raise SystemExit("Expected permissions.allow to be a JSON array.")
|
||||||
|
|
||||||
|
added = append_unique(allow, permissions_to_add)
|
||||||
|
|
||||||
if not added:
|
if not added:
|
||||||
print("Nothing to add — all rules already present.")
|
print("Nothing to add — all selected rules already present.")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"{'Would add' if dry_run else 'Adding'} {len(added)} rule(s):")
|
print(f"{'Would add' if args.dry_run else 'Adding'} {len(added)} rule(s):")
|
||||||
for rule in added:
|
for rule in added:
|
||||||
print(f" + {rule}")
|
print(f" + {rule}")
|
||||||
|
|
||||||
if dry_run:
|
if args.dry_run:
|
||||||
print("\nDry run — no changes written.")
|
print("\nDry run — no changes written.")
|
||||||
return
|
return
|
||||||
|
|
||||||
allow.extend(added)
|
atomic_write_json(SETTINGS_PATH, settings)
|
||||||
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(SETTINGS_PATH, "w") as f:
|
|
||||||
json.dump(settings, f, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
|
|
||||||
print(f"\nDone. {len(added)} rule(s) added to {SETTINGS_PATH}")
|
print(f"\nDone. {len(added)} rule(s) added to {SETTINGS_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user