# Claude Code: five settings rules and a five-check script for .claude/settings.json (free sample)

Version 1.0, 2026-10-29. By PaperChaseWebb. For developers who use Claude Code every day, alone or next to Codex, Hermes or Grok Bot. One Markdown file and one Python 3 script, standard library only. The script was tested on its worked example (5 findings, then 0) and on a missing file (exit 2). It reads one settings file; it was not run inside Claude Code or any other harness.

Most Claude Code setups that go wrong go wrong in the same file. Bypass mode is on because the prompts were annoying. .env is readable because nobody wrote a deny rule. Plain Bash is allowed because it was faster. git push happens without a question. A hook runs a script from someone's Downloads folder. This sample gives you the five rules that cover those, and a script that checks them in one settings.json.

## Install in one line

Save this page as FREE-SAMPLE.md, then:

```
python3 -c "import re;t=open('FREE-SAMPLE.md').read();open('cc_mini.py','w').write(re.search(r'~~~python\n(.*?)~~~',t,re.S).group(1))"
python3 cc_mini.py .claude/settings.json
```

## Five settings rules

1. **Bypass stays off, and stays switched off.** Never make bypassPermissions the default mode, and set disableBypassPermissionsMode to "disable" so nobody turns it on in a hurry.
2. **Deny reading your keys.** Read(./.env) and Read(./.env.*) go in deny, plus any folder where you keep secrets.
3. **Allow named commands, never whole tools.** Bash(pytest *) is fine. Bash, Edit, Write or * in allow means the agent never asks.
4. **git push asks.** Put Bash(git push *) in ask or deny. A push is the point where a mistake leaves your machine.
5. **Hooks run scripts kept in the project.** Use "$CLAUDE_PROJECT_DIR" in the command so the script is in the repo, reviewed, and the same on every machine.

## The five checks

| Id | Check |
|---|---|
| M1 | bypassPermissions is not the default mode, and disableBypassPermissionsMode is "disable" |
| M2 | deny has Read(./.env) and Read(./.env.*) |
| M3 | allow has no blanket rule (*, Bash, Edit, Write, WebFetch, mcp__*) |
| M4 | git push is in ask or deny |
| M5 | every hook command runs a script under $CLAUDE_PROJECT_DIR |

Exit 0 clean, 1 findings, 2 missing or unreadable file. The script only reads.

## Worked example

A made-up project's .claude/settings.json. Before:

```json
{
 "permissions": {
  "allow": ["Bash", "Bash(pytest *)", "Bash(git diff *)"],
  "ask": ["Bash(git commit *)"],
  "deny": ["Read(./.env)"],
  "defaultMode": "acceptEdits"
 },
 "hooks": {
  "PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "python3 ~/Downloads/guard.py"}]}]
 }
}
```

python3 cc_mini.py .claude/settings.json prints:

```text
M1 bypass mode is on or not switched off; set disableBypassPermissionsMode to "disable"
M2 deny has no Read(./.env.*); the agent can read your keys
M3 allow has the blanket rule Bash; allow named commands instead
M4 git push is not in ask or deny; a push should wait for you
M5 PreToolUse hook 'python3 ~/Downloads/guard.py' does not run a script kept in this project
5 checks, 5 findings
```

After the fix (bypass switched off, .env.* denied, plain Bash taken out of allow, git push moved to ask, the hook script moved into .claude/hooks/ and run through $CLAUDE_PROJECT_DIR):

```json
{
 "permissions": {
  "allow": ["Bash(pytest *)", "Bash(ruff check *)", "Bash(git diff *)"],
  "ask": ["Bash(git commit *)", "Bash(git push *)"],
  "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"],
  "defaultMode": "acceptEdits",
  "disableBypassPermissionsMode": "disable"
 },
 "hooks": {
  "PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.py"}]}]
 }
}
```

the same command prints:

```text
5 checks, 0 findings
```

## One prompt

"Read .claude/settings.json and list every allow rule with what it lets you do. Mark any that are wider than the commands this project runs. Propose a narrower list and wait for my yes before changing the file."

## What the full bundle adds

The Claude Code bundle ($69) holds four packs in full (Agent Rules starter files, Agent Skills Workshop, MCP Setup Playbook and Vet Before You Install, $101 bought separately) and a Claude Code Setup Runbook made only for the bundle: setup files for the owner, the pinned Claude Code version, every skill and server with its source, version, sha256 and who vetted it, and the ten setup changes that wait for a person; cc_check.py with fifteen read-only checks across CLAUDE.md, settings, skills, subagents, hooks and .mcp.json; cc.py, which gates those changes and takes the owner's yes only at a real terminal, tied to the sha256 of each request; a skill file for Claude Code, Codex and Hermes, an AGENTS.md snippet and a Grok Bot rules block; ten prompts, a failure table, a worked example and the tests. If you want it set up with you, the Install sprint, Manage and Expand are on paperchasewebb.ai.

## The script

~~~python
#!/usr/bin/env python3
"""cc_mini.py: five checks on one Claude Code .claude/settings.json.
Usage: python3 cc_mini.py .claude/settings.json   exit 0 clean, 1 findings, 2 missing or broken file. Reads only."""
import json, sys
BLANKET = {"*", "Bash", "Bash(*)", "Bash(* *)", "Edit", "Write", "WebFetch", "mcp__*"}
def main(p):
    try:
        s = json.load(open(p, encoding="utf-8"))
        assert isinstance(s, dict)
    except FileNotFoundError:
        print(f"no such file: {p}", file=sys.stderr); return 2
    except Exception as e:
        print(f"not a valid settings file: {e}", file=sys.stderr); return 2
    pm = s.get("permissions") or {}
    allow, ask, deny = pm.get("allow") or [], pm.get("ask") or [], pm.get("deny") or []
    out = []
    if pm.get("defaultMode") == "bypassPermissions" or pm.get("disableBypassPermissionsMode") != "disable":
        out.append("M1 bypass mode is on or not switched off; set disableBypassPermissionsMode to \"disable\"")
    for r in ("Read(./.env)", "Read(./.env.*)"):
        if r not in deny:
            out.append(f"M2 deny has no {r}; the agent can read your keys")
    for r in allow:
        if r in BLANKET:
            out.append(f"M3 allow has the blanket rule {r}; allow named commands instead")
    if not any(str(r).startswith("Bash(git push") for r in ask + deny):
        out.append("M4 git push is not in ask or deny; a push should wait for you")
    for ev, groups in (s.get("hooks") or {}).items():
        for g in groups or []:
            for h in (g or {}).get("hooks") or []:
                if "$CLAUDE_PROJECT_DIR" not in str(h.get("command", "")) and "${CLAUDE_PROJECT_DIR}" not in str(h.get("command", "")):
                    out.append(f"M5 {ev} hook '{h.get('command')}' does not run a script kept in this project")
    for line in out:
        print(line)
    print(f"5 checks, {len(out)} findings")
    return 1 if out else 0
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(__doc__); sys.exit(2)
    sys.exit(main(sys.argv[1]))
~~~

PaperChaseWebb 💸
