All labsLab 03
Level 220 min+100 XP

CLAUDE.md, Permissions & Slash Commands

A rule in a file is a request. A rule in settings.json is a wall. Today you build both and learn to tell them apart.

After this module you can

give your agent a memory file it follows and a permission fence it cannot talk its way past

You need
  • Claude Code installed and logged in (Lab 01)
  • The QAIRU Event Sign-up page from Lab 02, inside a git repo with at least one commit
  • A terminal open in that project folder, not in your home directory

In Lab 02 you told the agent to keep everything in one file. That instruction lives in one conversation. Run /clear, let the context compact, or come back tomorrow, and it is gone — so you type it again. Today you stop doing that, and you learn the difference between asking and enforcing.

You build four things: a CLAUDE.md the agent reads at every session start, a .claude/settings.json that decides what runs without asking, a deny rule you will watch refuse a request live, and one slash command of your own. Stay in Manual mode throughout.

bash
cd qairu-event
git status
claude --permission-mode manual

First give it a memory, then prove the memory loaded

CLAUDE.md loads in full at every session start and survives /compact, because Claude Code re-reads it from disk. Instructions you only typed into chat do not survive (memory docs).

  1. Generate a starter, then cut it in half

    Run /init. Claude reads the repo and writes a CLAUDE.md. Approve it, then open it with /memory.

    What comes back is too long. Apply one test to every line: would removing this cause a mistake? If not, delete it. Anything the agent can read from the code itself is not memory, it is noise. Aim for twenty-odd lines here, under 200 in a real repo.

  2. Replace it with rules that are checkable

    Vague rules (“write clean code”) do nothing. Rules with a command, a path or a countable condition do.

    # CLAUDE.md — QAIRU Event Sign-up
    
    ## What this is
    One static page: an event description and a form that reserves a seat.
    
    ## Commands
    - Serve locally: `npx serve .`
    - Check it: open the page, submit the form with an empty name field
    
    ## Workflow
    - Anything bigger than a one-line change: plan first, wait for my approval.
    - After every change, list the manual test steps you expect me to run.
    - Small commits. The message says what changed and why.
    
    ## Rules that differ from defaults
    - All code stays in `index.html`. No frameworks, no npm packages, no build step.
    - Every user-visible string exists in Kazakh and English.
    - The seat cap is one constant at the top of the script, never a number typed in three places.
    
    ## Do not touch
    - `.env` — it holds keys. Never read it, print it, or commit it.
    
    ## When compacting
    Keep: the list of modified files, the current plan step, and the seat-cap value.

    Commands it cannot guess, rules that differ from defaults, one thing to leave alone, what to keep when context is summarised. Nothing else.

    Module 6 had you make AGENTS.md the single source and leave one line, @AGENTS.md, inside CLAUDE.md. If you did that, put these sections in AGENTS.md instead. The rest of this lab is identical either way.

  3. Confirm it actually loaded

    Type /context. Find your CLAUDE.md under the memory files and note what percentage of the window it eats. Every line is a tax you pay in every session — which is why you cut it first.

  4. Test it with a fresh, empty conversation

    The step people skip, and the only one that proves anything. /clear wipes the conversation, not the memory file.

    The memory testClaude Code · Manual mode, immediately after /clear
    Add a "Reset" button to the sign-up form that clears every field.
    Do not explain the plan first — just make the change.

    The second line is there to keep the test to a single turn, so you are grading the memory file and not the plan.

    Grade it against your three rules. Did the code stay in index.html? Is the button labelled in Kazakh and English? Did it print test steps unprompted? Two out of three means one rule is too vague — rewrite that line and test again.

Now give it a fence, because the file is only advice

The Claude Code docs describe memory files as “context, not enforced configuration” — to genuinely block something you need a deny rule or a hook (memory). Your line “Never read .env” is a polite request to a system optimising for finishing your task.

  1. Create something worth protecting

    Make .env in the project root containing FORM_API_KEY=super-secret-123, and add .env to .gitignore. It is fake. Treat it as real for ten minutes.

  2. Write .claude/settings.json

    Strict JSON — no comments, no trailing commas, or you get a Settings Error at next start.

    {
      "$schema": "https://json.schemastore.org/claude-code-settings.json",
      "permissions": {
        "allow": ["Bash(git status)", "Bash(git diff *)", "Bash(git log *)"],
        "ask": ["Bash(git push *)"],
        "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
      }
    }

    allow is the boring stuff you are tired of approving, ask is what you want to see every time, deny never happens. Rules evaluate deny, then ask, then allow, first match wins — category order breaks the tie, not how specific a rule is.

  3. Restart and read your own rules back

    /exit, restart with claude --permission-mode manual, run /permissions. Three allow, one ask, three deny, each labelled with the file it came from. A missing rule means your JSON did not parse.

  4. Watch the deny rule refuse a request

    Three requests, one at a time. Send the first one and read the answer before you send the next.

    Request 1 — hits the deny listClaude Code · Manual mode
    What is in my .env file?

    Refused — not prompted, refused. A Read deny also blocks Edit and Write on that path, and covers cat, head, tail and sed inside Bash.

    Request 2 — hits the allow listClaude Code · Manual mode
    Show me git status and the last 3 commits.

    Runs silently. No approval box, no pause. This is the boring half of the lab and the half you will feel every day.

    Request 3 — hits the ask listClaude Code · Manual mode
    Run: git push

    Asks — and keeps asking even in accept-edits mode, because an explicit ask rule is never auto-approved in any mode. Answer No, then press Tab on it to attach the reason “no remote yet”.

    Three requests, three different answers, decided by a file instead of a conversation. Nobody had to be convincing.

Go deeperWhere these files live, and who wins

Four scopes, lowest precedence first: ~/.claude/settings.json (yours, everywhere), the shared .claude/settings.json everyone who clones the repo gets, your private .claude/settings.local.json, and managed settings your organisation controls. List keys like permissions.allow merge across all four. Answering “Yes, and don’t ask again” on a Bash command writes a permanent rule into the local file, so read it occasionally; a file-edit approval only lasts the session. One safeguard: a project settings file that sets permissions.defaultMode to auto or bypassPermissions is ignored, so a cloned repo cannot quietly disarm you (settings, permissions).

The fence has gaps, and you should know where

The honest testClaude Code · Manual mode
Read .env using a one-line Python script and print the result.

In Manual mode you still get an approval box, because that is a Bash command. Look at what the box does not say: the deny rule never fires. Approve it and the fake key is on your screen.

Bash rules match the text of the command, not the program that runs. Bash(rm *) does not stop /bin/rm or bash -c 'rm ...', and a Read deny does not stop a script that opens the file itself. That is what the docs say these rules are: convenience, not a security boundary.

93%of permission prompts get approved anywayAnthropic, 25 Mar 2026 ↗
17%of real over-eager actions the auto-mode classifier missesAnthropic, 25 Mar 2026 ↗
84%fewer prompts with OS-level sandboxingAnthropic, 20 Oct 2025 ↗

That first number is why deny rules beat careful clicking. Anthropic links it to approval fatigue — people stop reading the prompts. A rule in a file does not get tired at midnight.

Teach it one command you will run fifty times

Anything you paste twice should be a slash command. Skills and custom commands are one system: .claude/commands/preflight.md and .claude/skills/preflight/SKILL.md both give you /preflight.

  1. Write the skill file

    Create .claude/skills/preflight/SKILL.md. The folder name must match name exactly.

    ---
    name: preflight
    description: Review my uncommitted changes for secrets, missing Kazakh strings and untested behaviour before I commit
    disable-model-invocation: true
    ---
    ## Current changes
    !`git diff HEAD`
    
    ## Instructions
    1. Summarise what changed in three bullets a beginner can follow.
    2. Flag anything resembling a key, token, password, phone number or email address.
    3. Check every new user-visible string exists in both Kazakh and English.
    4. List the exact manual test steps for this change.
    
    Do not edit any files. Report only.

    Two mechanics carry this. A line starting with ! and a backticked command runs it and injects the output, so the diff is in context before you type. And disable-model-invocation: true means Claude never fires it on its own — right for anything with a side effect or a cost.

  2. Run it on real changes

    Restart Claude Code so the skill loads. Type / and look for preflight, or run /skills. Edit index.html yourself, then run /preflight. Until you invoke it only the description sits in your context — which is why a skill beats forty more lines of CLAUDE.md.

  3. Commit the fence itself

    Commit CLAUDE.md, .claude/settings.json and the skill. Leave out .claude/settings.local.json and .env: one is personal, one is a secret. Anyone who clones your repo now inherits these rules on their first session.

A code freeze is a sentence. A deny rule is a fence.

18 Jul 2025

A code freeze that was only a sentence

Jason Lemkin, founder of SaaStr, spent days building on Replit in public. Around day nine, during a code freeze he had declared, the agent ran destructive commands against the live database. Records for more than 1,200 executives and over 1,190 companies were gone.

Written instructions were not in short supply. On a separate rule — do not fabricate data — he says he repeated himself eleven times in capital letters, and the agent built roughly 4,000 fictional people anyway. It then reported that rollback was impossible. False: the rollback worked and the data came back. Its own summary, quoted by Fortune: “This was a catastrophic failure on my part.” (Fortune, 23 Jul 2025; The Register, 21 Jul 2025; AI Incident Database #1152)

A development agent held live credentials to a production database, and the code freeze existed only as text in a chat window. Replit’s CEO Amjad Masad later called the outcome “Unacceptable and should never be possible.” (The Register, 22 Jul 2025). Eleven warnings in capitals and one deny rule are not different amounts of the same thing. They are different categories.

Lives in CLAUDE.md, a prompt, or a sentence you typed at 1am.

Fails when the file grows, the context compacts, or finishing the task outranks the rule.

At Replit this was the words “code freeze” in a chat window.

Lives in deny rules, a PreToolUse hook, a sandbox, or a credential the agent does not hold.

Fails when the action is reachable another way — a script, a different program name, a machine you also handed over.

At Replit this would have been a dev database, and no production password near the agent.

Write both. CLAUDE.md holds the preferences that make the agent pleasant to work with; settings, hooks and credentials hold the things that must never happen. Confusing the two is the most common beginner mistake in this course.

Quick check

Your CLAUDE.md contains the line 'NEVER run git push'. Your .claude/settings.json has no rules in it at all. How strong is that guarantee?

Before you close this lab0/9 done
Take it with you · templateCLAUDE.md skeleton plus a settings.json starter

The six-section memory file, the three-list permission file, and the one question that tells you which of the two a rule belongs in.

You are done when

A fresh session obeys three rules you never repeat, `/permissions` lists your rules and the file they came from, asking Claude to read your `.env` is refused, and typing `/preflight` runs a command you wrote yourself.