4 · Claude config — settings, modes, hooks
Objective — write the project-wide settings.json, pick a permission mode (strict / medium / yolo), set personal model/thinking/compact defaults in gitignored settings.local.json (never ~/.claude/settings.json), and wire the hard-block + vendor-edit guard hooks that no mode can override.
Background
Section titled “Background”This step controls how much friction the agent operates with and installs the safety net that catches destructive commands regardless of mode. The split between what’s committed and what stays personal matters — get it wrong and you either leak a teammate’s friction choice or commit nothing at all.
| File | Committed? | Purpose |
|---|---|---|
.claude/settings.json | ✅ C4 | Project-wide settings everyone shares — e.g. disabledMcpjsonServers for project .mcp.json servers |
.claude/settings.local.json | ❌ gitignored | Personal — permission mode plus per-project overrides for model, autoCompactEnabled, and env.MAX_THINKING_TOKENS (overrides global for this repo only) |
.claude/.gitignore | ✅ committed | Ignores settings.local.json and *.bak.* |
1. Write .claude/settings.json — trim project MCP noise
Section titled “1. Write .claude/settings.json — trim project MCP noise”Two MCP scopes — don’t conflate them:
| Scope | Where | How to trim |
|---|---|---|
| User / machine | Registered in machine setup §3 (claude mcp add -s user) | /mcp toggles or user-level config — not this file |
| Project | Committed .mcp.json (e.g. Laravel Boost) | disabledMcpjsonServers in .claude/settings.json — Claude Code docs |
Only disable servers you do not want auto-approved from the project .mcp.json. User-scope servers (ZajLibrary, Cloudflare, Stripe, …) are trimmed on the machine, not duplicated here.
-
Read exact server names for project
.mcp.jsonentries. If Cursor & other IDEs or kitseed.shalready created.mcp.json, run/mcpand use names exactly as shown. Hand path before Cursor wiring: use an empty array and revisit after Boost is registered.- ✅
/mcplists each project-scoped server you registered in.mcp.json.
- ✅
-
Write
.claude/settings.json— disable only project.mcp.jsonservers this repo does not need.{"disabledMcpjsonServers": ["server-key-from-mcp-json-if-unused"]}- ✅ Valid JSON. For a typical CodeCanyon Laravel project you enable Laravel Boost from
.mcp.jsonand leave the array empty or minimal. User-scope MCPs from machine setup are out of scope for this key.
- ✅ Valid JSON. For a typical CodeCanyon Laravel project you enable Laravel Boost from
-
Verify on disk — commit at end of this page (C4).
Terminal window test -f .claude/settings.json && python3 -m json.tool .claude/settings.json >/dev/null && git add -n .claude/settings.json# Expected: JSON validates; dry-run lists the file — commit in §7 below- ✅
.claude/settings.jsonis in place (written now; committed at C4).
- ✅
2. Add the committed .gitignore
Section titled “2. Add the committed .gitignore”Keep the personal permission mode and timestamped backups out of git so each developer’s friction choice stays local.
settings.local.json*.bak.*# Expected: settings.local.json and rotated backups stay untracked- ✅ Once git exists (Create the project bootstrap),
git statusno longer surfacessettings.local.jsonor*.bak.*files.
3. Pick a permission mode — strict / medium / yolo
Section titled “3. Pick a permission mode — strict / medium / yolo”settings.local.json carries your permission mode. The kit ships three presets and a set-claude-mode.sh switcher that installs one — with a timestamped backup and an allow-list merge so your accumulated approvals survive the switch.
| Mode | defaultMode | Posture | Use when |
|---|---|---|---|
| strict | plan | Read-only. No Bash/Edit/Write. Proposes diffs. | Security/deploy work, code review, unfamiliar code |
| medium | acceptEdits | Auto-allows edits + safe git/composer/npm/artisan. Asks for unknown Bash. | Default for active dev |
| yolo | bypassPermissions | Auto-allows almost everything — but a PreToolUse hook hard-blocks destructive commands. | Local throwaway / heavy refactors. Never on prod branches or with real creds. |
-
Apply a mode from the project root.
Terminal window ./.claude/claude-mode/bin/set-claude-mode.sh medium # strict | medium | yolo | show# Expected: writes settings.local.json with a timestamped backup + allow-list merge- ✅ The switcher reports the mode applied and leaves a
*.bak.*backup.
- ✅ The switcher reports the mode applied and leaves a
-
Quit and restart the agent (👤 user) —
settings.local.jsonis read at session start, not hot-reloaded.- ✅ The agent reloads with the new mode active.
-
Verify in the new session.
Terminal window ./.claude/claude-mode/bin/set-claude-mode.sh show# Expected: prints the active mode (e.g. "medium")- ✅ A borderline command behaves correctly — in medium,
git statusauto-approves,git push --forceis denied,rm /tmp/xasks.
- ✅ A borderline command behaves correctly — in medium,
4. Set personal model + token defaults — merge into settings.local.json
Section titled “4. Set personal model + token defaults — merge into settings.local.json”Do not write these to ~/.claude/settings.json. Machine-wide MCP and gh auth belong on machine setup; model, thinking budget, and auto-compact are project/personal choices that belong here — gitignored, per developer, and scoped to this repo via Claude Code precedence: enterprise → CLI → .claude/settings.local.json → .claude/settings.json → global.
Run after §3 so the switcher has already created .claude/settings.local.json (permissions + mode). Then deep-merge the productivity keys only — same pattern as the old global merge, but local so your global hooks, plugins, and default model stay untouched.
-
Ensure
jqis present.Terminal window command -v jq &>/dev/null && echo "jq present" || brew install jq# Expected: "jq present"- ✅
command -v jqsucceeds.
- ✅
-
Deep-merge model + token keys into the existing local file (never replace the whole file — that drops permissions from §3).
Terminal window LOCAL=".claude/settings.local.json"test -f "$LOCAL" || { echo "run set-claude-mode.sh first (§3)"; exit 1; }jq '.model = "opus"| .autoCompactEnabled = true| .env = ((.env // {}) + {"MAX_THINKING_TOKENS": "12000"})' \"$LOCAL" > "${LOCAL}.tmp" && mv "${LOCAL}.tmp" "$LOCAL"# Expected: permissions block from §3 intact; model + compact + env merged- ✅
jq '.model, .autoCompactEnabled, .env.MAX_THINKING_TOKENS, .defaultMode' "$LOCAL"prints your model,true,"12000", and the mode from §3. - ✅ Swap
"opus"for"sonnet"(or another supported id) if you prefer cost over depth — this file is gitignored and never imposed on teammates.
- ✅
-
Restart the agent again so model + env load with the permission mode from §3.
- ✅ A fresh session uses the merged local file; global
~/.claude/settings.jsonwas not modified.
- ✅ A fresh session uses the merged local file; global
- ✅
model— e.g. Opus for agentic Laravel work; overrides global for this project only. - ✅
MAX_THINKING_TOKENS— caps extended-thinking spend (merged into existingenv, not replacing other vars). - ✅
autoCompactEnabled— compacts context before overflow.
See also the kit’s .claude/settings.local.json.example (AI System kit).
Which mode for which phase
Section titled “Which mode for which phase”The friction should match the risk of the work. A sensible default mapping across the playbook:
| Work | Recommended mode | Why |
|---|---|---|
| Setup, active dev, app config (Phases 1–3, 6, 8–9) | medium | Edits flow; unknown Bash still pauses |
| Heavy local refactors on a throwaway branch | yolo | Speed; the hard-block hook still guards. Never with real creds or on a prod branch |
| Deploy, staging, security, audit, pre-customer, production (Phases 4–5, 7, 10–11, P) | strict | Read-only by default; the agent proposes diffs you approve |
Session-only switch — /permission-mode
Section titled “Session-only switch — /permission-mode”To change posture for the current session only (without editing settings.local.json or restarting), use the in-session /permission-mode command. It’s the right tool for a one-off — e.g. dropping to plan to review a risky diff, then back. The set-claude-mode.sh switcher is for the persistent default that survives restarts.
What medium looks like (excerpt)
Section titled “What medium looks like (excerpt)”{ "_mode": "medium", "defaultMode": "acceptEdits", "permissions": { "allow": ["Read","Grep","Glob","Edit","Write", "Bash(git status*)","Bash(git add *)","Bash(git commit *)", "Bash(composer install*)","Bash(npm run *)","Bash(php artisan *)", "Bash(vendor/bin/pest*)","Bash(vendor/bin/pint*)"], "deny": ["Bash(rm -rf /)","Bash(sudo *)","Bash(git push --force*)", "Bash(php artisan migrate:fresh*)","Bash(php artisan db:wipe*)", "Write(.env*)","Edit(.env*)"], "ask": ["Bash(rm *)","Bash(git push*)","Bash(ssh *)","Bash(mysql *)", "Bash(php artisan migrate:rollback*)","Bash(php artisan db:seed*)"] }}The full strict / medium / yolo presets ship in the kit’s claude-mode/resources/templates/.
Env-template read posture for 1Password
Section titled “Env-template read posture for 1Password”The 1Password path in Project constitution §5 only works if the agent can read safe templates and run safe op discovery/render commands. .env.example and .env.tpl are committed placeholders/references, not secrets; the real secret values live in .env or inside 1Password.
Use this default posture in the installed AI-System settings:
| Action | Default | Why |
|---|---|---|
Read(./.env.example) | Allow | Needed to enumerate intended env keys; contains placeholders only |
Read(./.env.tpl) / Read(./.env.*.tpl) | Allow | Contains op:// references only, no values |
Bash(op vault ls*) / Bash(op item ls*) | Allow | Discovers vault/item names without printing field values |
Bash(op inject*) | Allow | Renders .env from references; command output should not print values |
Edit(.env*) / Write(.env*) | Deny or ask | Real env files contain secrets; writing them should stay controlled |
Bash(op item get * --format=json*) | Ask or deny | Can print full item contents, including secrets |
Ship this project allow block in .claude/settings.local.json (merge into your chosen mode preset) so Project constitution §5 Option A can run unattended after the global posture is fixed:
{ "permissions": { "allow": [ "Read(./.env.example)", "Read(./.env.tpl)", "Read(./.env.*.tpl)", "Bash(op vault ls*)", "Bash(op item ls*)", "Bash(op inject*)" ], "deny": [ "Write(.env*)", "Edit(.env*)", "Bash(op item get * --format=json*)" ] }}Human/installer global fix (pick one, then restart Claude Code):
| Variant | Change in ~/.claude/settings.json → permissions.deny |
|---|---|
| Dev-friendly (owner default) | Remove Read(./.env) and Read(./.env.*); keep write blocks + the hook |
| Conservative | Replace Read(./.env.*) with explicit real-env paths only (.env, .env.local, .env.production, .env.staging, .env.*.local) so .env.example / .env.tpl stay readable |
5. Wire the hard-block hook — block-destructive.sh
Section titled “5. Wire the hard-block hook — block-destructive.sh”The reason yolo is safe: a PreToolUse hook runs above the permission system and blocks destructive commands even under bypassPermissions. It’s wired into the yolo preset.
-
Wire it into the yolo preset so it fires on every Bash call.
{"defaultMode": "bypassPermissions","hooks": {"PreToolUse": [{"matcher": "Bash","hooks": [{ "type": "command", "command": ".claude/claude-mode/hooks/block-destructive.sh" }]}]}}# Expected: the hook runs on every Bash tool call, above the permission system- ✅ The hook reads the command from stdin and exits
2(hard block) on patterns likerm -rf /,sudo,git push --force,chmod 777,curl | bash,migrate:fresh|wipe|reset,DROP TABLE,::truncate(, and deletions ofdatabase/migrationsor owned packages. Exit2is the highest-precedence signal — it can’t be overridden from inside a running session.
- ✅ The hook reads the command from stdin and exits
-
Test the hook standalone (no agent needed).
Terminal window printf '%s\n' '{"tool_name":"Bash","tool_input":{"command":"php artisan migrate:fresh"}}' \| ./.claude/claude-mode/hooks/block-destructive.shecho "exit=$?" # → exit=2printf '%s\n' '{"tool_name":"Bash","tool_input":{"command":"git status"}}' \| ./.claude/claude-mode/hooks/block-destructive.shecho "exit=$?" # → exit=0# Expected: the hook parses the modern tool_name/tool_input payload,# blocks a migration-reset command, and allows a safe read-only command.The payload is only JSON passed to the hook. It does not run
php artisan migrate:fresh; the hook reads the command string and exits before any shell execution.- ✅ The destructive test payload reports
exit=2(hard block);git statusreportsexit=0(auto-approve).
- ✅ The destructive test payload reports
Precedence — which rule wins
Section titled “Precedence — which rule wins”- PreToolUse hook exit 2 → hard block (overrides everything, even bypass mode)
permissions.deny→ block- PreToolUse hook exit 1 → force prompt
permissions.ask→ promptpermissions.allow→ auto-approve- PreToolUse hook exit 0 → auto-approve
defaultModebehavior
6. Add the vendor-edit guard hook (optional but recommended)
Section titled “6. Add the vendor-edit guard hook (optional but recommended)”CodeCanyon work lives or dies on not silently editing vendor code. A lightweight PostToolUse hook reminds you to wrap in-place vendor edits in ZAJ:BEGIN/END markers and log them in _CUSTOMIZATIONS.md.
The hook fires whenever a file under vendor/, app/ core, or a vendor view is touched. The kit ships hooks/vendor-edit-reminder.sh and the wiring; it’s advisory (exit 0 with a stderr note), not a block.
- ✅ Editing a
vendor/file triggers the reminder note without blocking the edit.
Secret-handling reference (apply across every phase)
Section titled “Secret-handling reference (apply across every phase)”This block is the single source of truth for how secrets are handled in any AI-assisted workflow (Claude Code, Cursor, Copilot CLI — any agent that reads terminal output). It’s seeded as feedback_secret_handling.md in Rules & skills so the agent auto-loads it; the canonical copy lives here.
The 5 rules:
-
Exit-code verification, not value verification. To check “does this secret exist?”, use exit code 0/1 — never print the value.
Terminal window # ✅ GOOD — exit code only, length-only feedback[ -n "$TOKEN_VAR" ] && echo "✅ present (${#TOKEN_VAR} chars, value hidden)" || echo "❌ missing"some-getter >/dev/null 2>&1 && echo "✅ configured" || echo "❌ missing"# ❌ BAD — leaks the secret into the conversation logecho "$TOKEN_VAR"echo "$TOKEN_VAR" | head -c 15composer config --global github-oauth.github.com # a GETTER — prints the tokencat ~/.config/composer/auth.json -
Length is safe to print; any substring is not.
${#VAR}prints the character count only. Even 4 characters of a real token (ghp_,gho_,sk_live_) identifies the provider and type, and ~15 characters is enough to find it in logs. Never print any slice of a secret. -
Redirect stdout when setting secrets. Many CLIs echo back the value they just stored.
Terminal window # ✅ GOOD — stdout + stderr silencedcomposer config --global github-oauth.github.com "$(gh auth token)" >/dev/null 2>&1# ❌ BAD — composer prints the token back after setting itcomposer config --global github-oauth.github.com "$(gh auth token)" -
Command substitution and exported vars are safe; echoing them is not.
$(gh auth token)and$CF_API_TOKENexpand at execution and are passed as an argument / env var — not printed; shell history stores the literal$(gh auth token), not the value. The danger is only a downstream command that round-trips the value back:echo,printf,cat, acurlwithout-o /dev/null, a getter CLI. Treat the expansion as safe; treat any tool that might print the value as hostile. -
Never ask the user to “paste the token to verify it.” Asking for a secret in chat is the leak. Run the exit-code check and report the length. If the user offers to paste, refuse and point them at the length check.
Patterns by secret type:
| Secret | Where it lives | ✅ Safe verification | ❌ Dangerous commands |
|---|---|---|---|
| GitHub token | ~/.config/composer/auth.json | composer config --global github-oauth.github.com >/dev/null 2>&1 && echo "✅" | composer config --global github-oauth.github.com (prints it) |
| Cloudflare API token | ~/.zshrc env var | [ -n "$CF_API_TOKEN" ] && echo "✅ (${#CF_API_TOKEN} chars)" | echo $CF_API_TOKEN, echo $CF_API_TOKEN | head -c N |
| Database password | .env DB_PASSWORD=… | grep -q '^DB_PASSWORD=' .env && echo "✅ set" | grep DB_PASSWORD .env, cat .env |
| Stripe secret key | .env STRIPE_SECRET=… | grep -q '^STRIPE_SECRET=sk_' .env && echo "✅ looks like a key" | grep STRIPE .env, cat .env |
| SMTP password | .env MAIL_PASSWORD=… | grep -q '^MAIL_PASSWORD=.\+' .env && echo "✅ set" | grep MAIL .env |
| Webhook signing secret | .env, provider dashboard | grep -q '^STRIPE_WEBHOOK_SECRET=whsec_' .env && echo "✅" | grep WEBHOOK .env |
| Redis / Upstash token | .env REDIS_PASSWORD=… | grep -q '^REDIS_PASSWORD=.\+' .env && echo "✅ set" | grep REDIS .env |
| 1Password-stored secret | 1Password vault | op item get "<item>" --fields label=<field> >/dev/null && echo "✅" | op item get "<item>" --format=json (prints everything) |
7. Commit Claude config (C4)
Section titled “7. Commit Claude config (C4)”-
Stage and commit C4 — settings + committed
.gitignore; add vendor-edit hook paths if wired outsideclaude-mode/.Terminal window git branch --show-current # must be developgit add .claude/settings.json .claude/.gitignore# If vendor-edit guard lives outside claude-mode/: git add .claude/hooks/git commit -m "feat(ai): claude settings + destructive/vendor-edit guards"git log --oneline -4 # C4 atop C3, C2, C1# Expected: C4 on develop; settings.local.json not staged- ✅
git check-ignore .claude/settings.local.jsonstill succeeds; C4 appears ingit log.
- ✅
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🔀
settings.jsonon disk — trimmed project MCP list usingdisabledMcpjsonServerswhere needed; C4 committed. - 🤖
.claude/.gitignorein place — ignoressettings.local.jsonand*.bak.*;git statusno longer surfaces them. - 🔀 Personal defaults merged —
.claude/settings.local.jsonhas your chosenmodel,autoCompactEnabled, andenv.MAX_THINKING_TOKENSviajqmerge; global~/.claude/settings.jsonuntouched; permissions from §3 intact. - 🔀 Permission mode applied + verified —
set-claude-mode.sh showreports the mode in a new session; a borderline command behaves correctly. - 🤖 Block-destructive hook tested —
php artisan migrate:freshexits2,git statusexits0via the standalone JSON-payload test. - 🤖 Vendor-edit guard wired (optional) — touching a
vendor/file triggers the advisory reminder. - 🤖 C4 committed on
develop—.claude/settings.json+.claude/.gitignore;settings.local.jsongitignored.