Skip to content

Course · Claude Code · Permissions & modes

Lock the agent out with deny rules and protected paths

Loose sessions are where mistakes get expensive. The course’s small TypeScript payments fixture does not ship secrets. For this isolated exercise, create three disposable placeholders in your own copy: .env, secrets/example.txt, and config/production.json. Do not put real credentials in them. If the session is already running loose - acceptEdits on, test commands allowed - then one careless prompt is enough for the agent to read sensitive-looking data into its context or rewrite a protected config file. What you want here is not caution-by-prompt. It is a wall.

The tempting move is to say it: “don’t touch anything in secrets/.” In the moment, the agent may respect that. But a sentence in the conversation is guidance, not enforcement. It lives in a context window that can be compacted, cleared, and resumed, and it competes with every other instruction in the session. For anything that matters, you want a rule.

A deny rule is a settings entry that moves an action to forbidden: not “ask first” but “refuse”. You write it in .claude/settings.json, alongside any allow rules:

.claude/settings.json
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./secrets/**)",
"Edit(./config/production.*)"
]
}
}

Now the wall is real. Illustrative exchange:

> check what database URL we use in the env file
⚠ Read(./.env) is denied by your permission rules. I can't open that file.
If you need a value from it, paste the specific line you want me to use.

Delete the three placeholders when you finish the exercise. They exist only to make the deny rules observable; they are not part of the companion fixture.

Note the two verbs. A Read deny keeps contents out of the agent’s context; an Edit deny blocks modification. The rule lives in its settings scope, so it survives compaction and new sessions, and the current permission-modes docs state that deny rules block in every permission mode, including bypassPermissions. One limit to keep in mind: a Read deny gates the Read tool, not the filesystem. A shell command such as cat .env reaches the same bytes through whatever Bash rules allow, so wall the shell side with the same Bash(...) patterns when the secret matters. The full matcher syntax is in the permissions docs.

You do not start from zero. Claude Code ships with a set of protected paths guarded in normal permission modes: your .git directory, its own .claude configuration, shell startup files such as .zshrc, your .mcp.json; the current list lives in the permission-modes reference. bypassPermissions is the important exception and can skip those prompts, which is one reason the hands-off lesson confines it to containers. Your deny rules extend protection to the paths your project considers sacred. Truly sensitive work still needs defenses outside the permission system - least-privilege credentials, network controls, and the sandboxing guidance - because a permission rule governs what the agent may call, not what the credentials on the machine can do.

This is the real shape of operating an agent well: not a single global trust level, but a high floor and a low ceiling at once. Loose where it is safe - edits auto-applied, tests running free. Walled where it is not - secrets unreadable, production config untouchable. Both halves in one session is the whole skill.

Every loosening so far still kept you in the room. A long batch you want to walk away from raises a sharper question than any prompt: how do you let the agent run with nobody watching without handing it the keys to the whole machine?