Advanced: harness settings
"Harness" means Claude Code itself: permissions, hooks, environment, memory. You configure it with settings.json files.
Three layers
| File | Scope | Committed? |
|---|---|---|
~/.claude/settings.json (or $CLAUDE_CONFIG_DIR/settings.json) |
You, every project | No |
<repo>/.claude/settings.json |
Everyone on this repo | Yes |
<repo>/.claude/settings.local.json |
You, this repo only | No (gitignore it) |
Narrower layers override broader ones. Deny rules from any layer still apply. Each account folder has its own user-level file, so work and personal can differ.
Permissions
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": ["Bash(git status:*)"],
"deny": ["Bash(git push --force:*)"]
}
}
Modes: default (ask), acceptEdits, plan (read-only), bypassPermissions (no prompts). See templates/settings/README.md for the trade-offs and the safety rails that must come with bypass.
Rule syntax is Tool(pattern). :* after a command prefix means "any arguments". Keep allow-lists short and specific. A long allow-list is a sign you want acceptEdits plus a few rules.
Other useful keys: autoMemoryEnabled, env (variables for every session), enabledPlugins, hooks, statusLine.
Hooks
A hook is a command the harness runs on an event. Claude cannot skip it. This is how you turn "please never do X" into "cannot do X".
Common events:
| Event | When | Typical use |
|---|---|---|
PreToolUse |
Before a tool runs | Block dangerous commands |
PostToolUse |
After a tool runs | Run a formatter or tests |
UserPromptSubmit |
You send a prompt | Add context |
Stop |
Claude finishes a turn | Notify, log, save state |
The hook gets JSON on stdin. For PreToolUse, exit code 2 blocks the call and the stderr message goes back to Claude.
Example: block git push --force
~/.claude/hooks/block-force-push.sh:
#!/usr/bin/env bash
cmd="$(jq -r '.tool_input.command // empty')"
if echo "$cmd" | grep -Eq 'git +push.*(--force|-f( |$))|git +reset +--hard'; then
echo "Blocked: force-push and hard reset need the human." >&2
exit 2
fi
exit 0
Make it executable (chmod +x), then register it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "$HOME/.claude/hooks/block-force-push.sh", "timeout": 5 }
]
}
]
}
}
Needs jq (brew install jq). Test it by asking Claude to run git push --force in a scratch repo. The git-guardrails skill can set up a fuller version for you.
Why hook AND deny rule? The deny rule matches exact prefixes. The hook can match patterns, read context, and log. Use both.
Status line
A status line is one line of text under the prompt, fed by a script. Ideas that help with multi-account work:
- Show the account:
basename "$CLAUDE_CONFIG_DIR". - Show the git branch and dirty flag.
- Show the permission mode, so you always see if you are in bypass.
{ "statusLine": { "type": "command", "command": "$HOME/.claude/statusline.sh" } }
The script receives session JSON on stdin and prints one line. Keep it fast.
Checklist
- Hooks and deny rules are the real safety net, not instructions in CLAUDE.md.
- Keep the user file small. Put project rules in the project.
- After editing settings, start a new session. Invalid JSON can silently disable the whole file: validate with
python3 -m json.tool file. - Commit
.claude/settings.jsonfor teams, neversettings.local.json.