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.
give your agent a memory file it follows and a permission fence it cannot talk its way past
- 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.
cd qairu-event
git status
claude --permission-mode manualFirst 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).
Generate a starter, then cut it in half
Run
/init. Claude reads the repo and writes aCLAUDE.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.
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.mdthe single source and leave one line,@AGENTS.md, insideCLAUDE.md. If you did that, put these sections inAGENTS.mdinstead. The rest of this lab is identical either way.Confirm it actually loaded
Type
/context. Find yourCLAUDE.mdunder 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.Test it with a fresh, empty conversation
The step people skip, and the only one that proves anything.
/clearwipes the conversation, not the memory file.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.
Create something worth protecting
Make
.envin the project root containingFORM_API_KEY=super-secret-123, and add.envto.gitignore. It is fake. Treat it as real for ten minutes.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.
Restart and read your own rules back
/exit, restart withclaude --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.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.
What is in my .env file?Refused — not prompted, refused. A
Readdeny also blocks Edit and Write on that path, and coverscat,head,tailandsedinside Bash.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.
Run: git pushAsks — and keeps asking even in accept-edits mode, because an explicit
askrule is never auto-approved in any mode. Answer No, then pressTabon 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
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.
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.
Write the skill file
Create
.claude/skills/preflight/SKILL.md. The folder name must matchnameexactly.--- 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. Anddisable-model-invocation: truemeans Claude never fires it on its own — right for anything with a side effect or a cost.Run it on real changes
Restart Claude Code so the skill loads. Type
/and look forpreflight, or run/skills. Editindex.htmlyourself, then run/preflight. Until you invoke it only the description sits in your context — which is why a skill beats forty more lines ofCLAUDE.md.Commit the fence itself
Commit
CLAUDE.md,.claude/settings.jsonand the skill. Leave out.claude/settings.local.jsonand.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.
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.
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.
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.