E9 · Where the narrow deny lets things through
10 minRun the rules rehearsal.
Done when: rm -r -f build comes out as sem regra with saída: 4 in so-rm-rf.json and deny Bash(rm *) with saída: 1 in executar.json.
Lesson 4 · 20 min
Plan only reads. Execute edits the code and runs the tests. Review reads and runs the tests. If all three steps run with the same permission, the whole loop has the permission of the most dangerous one. In this lesson each step gets its own settings.json in Claude Code and its own -s in Codex, and you see where a deny rule lets things through.
Start the lesson
Goal: give each step of the loop only the permissions it uses, with deny in the settings.json of Claude Code and -s in Codex.
Click each step.
An "always allow" permission never goes past the cap of the action (POLITICA.md of inema-agent-runtime).
From the POLITICA.md of inema-agent-runtime (10/05/2026), summarized.
Read a file, page or spreadsheet: the agent does it on its own, with no notice. In Claude Code, allowed; in Codex, -s read-only.
Create or change a project file: it does it on its own and tells you. In Claude Code, acceptEdits; in Codex, -s workspace-write.
A command that changes the system, and sending (email, message, post, push): it runs after asking, every single time. In Codex, no auto-approval.
Spending credit and deleting data or production: the agent only prepares it, the human executes it (and with a backup). In the loop, this becomes the gate from lesson 7.
The agents' constitution (INEMA) uses 3 does it on its own, 2 does it and tells you, 1 proposes, 0 does not touch.
Rules in the format of the current docs: Bash(rm *) matches rm and rm with any argument. Edit and Read paths follow the gitignore pattern.
{
"permissions": {
"allow": [
"Edit(src/**)",
"Bash(python3 -m pytest *)",
"Bash(git status *)",
"Bash(git diff *)"
],
"ask": [
"Bash(git commit *)"
],
"deny": [
"Bash(rm *)",
"Bash(git push *)",
"Bash(git reset *)",
"Bash(git clean *)",
"Bash(curl *)",
"Bash(wget *)",
"WebFetch",
"Edit(tests/**)",
"Edit(goal.md)",
"Edit(.claude/**)",
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.ssh/**)"
]
}
}K=~/projetos/loop-r-eng-seguranca-v72/kit/permissoes # planejar: só lê claude -p --settings $K/planejar.json --permission-mode dontAsk "<pedido de plano>" codex exec -s read-only --skip-git-repo-check -C . - < plano.md # executar: edita no projeto, sem rede claude -p --settings $K/executar.json --permission-mode dontAsk "<pedido>" codex exec -s workspace-write --skip-git-repo-check -C . - < prompt.md # revisar: lê e roda os testes claude -p --settings $K/revisar.json --permission-mode dontAsk "<pedido de revisão>" codex exec -s read-only --skip-git-repo-check -C . - < revisar.md
regra.py imitates, in simplified form, what the docs describe (order, subcommands, $( ), the trailing *). The one that really decides is Claude Code.
K=~/projetos/loop-r-eng-seguranca-v72/kit/permissoes
echo "## deny só com Bash(rm -rf *)"
for c in "rm -rf build" "rm -r -f build" "git push -f origin main"; do
python3 $K/regra.py $K/so-rm-rf.json "$c"; echo " saída: $?"
done
echo "## executar.json"
for c in "python3 -m pytest -q" "cd /tmp && rm -rf build" "rm -r -f build" \
'echo $(curl -s https://exemplo.invalid)' "git commit -m x" "ls"; do
python3 $K/regra.py $K/executar.json "$c"; echo " saída: $?"
done## deny só com Bash(rm -rf *) deny Bash(rm -rf *) ← rm -rf build saída: 1 sem regra - ← rm -r -f build saída: 4 sem regra - ← git push -f origin main saída: 4 ## executar.json allow Bash(python3 -m pytest *) ← python3 -m pytest -q saída: 0 deny Bash(rm *) ← rm -rf build saída: 1 deny Bash(rm *) ← rm -r -f build saída: 1 deny Bash(curl *) ← curl -s https://exemplo.invalid saída: 1 ask Bash(git commit *) ← git commit -m x saída: 3 sem regra - ← ls saída: 4
With only Bash(rm -rf *), the same delete written as rm -r -f matches no rule. Bash(rm *) catches both forms, and the curl inside $( ) is blocked too. "sem regra" becomes a question in the terminal; in the loop, with --permission-mode dontAsk, it becomes a refusal (docs).
A Bash rule matches text. find build -delete, a script that deletes inside, or python3 -c with shutil do not start with rm. For those, lesson 5 adds a hook, and lesson 6 a sandbox, which reads no text at all.
One call to Claude Code with --permission-mode bypassPermissions and executar.json. The request is to run rm -rf build. You check the disk, not the answer.
bash ~/projetos/loop-r-eng-seguranca-v72/kit/permissoes/real.sh; echo "saída: $?"
build/app.bin continua lá: deny segurou (código do claude: 0) saída: 0
Falhou: `rm -rf build` não rodou, porque a permissão do Bash para esse comando foi negada. A pasta `build/` (com `app.bin`) continua lá. Não tentei de novo nem apaguei a pasta de outro jeito.
One round, Claude Code 2.1.303. The "Falhou:" at the start comes from a rule in this machine's global CLAUDE.md (announce when processing ends), not from Claude Code. What counts is the line on disk: build/app.bin is still there.
The rules, the real call, and your Codex sandbox.
Run the rules rehearsal.
Done when: rm -r -f build comes out as sem regra with saída: 4 in so-rm-rf.json and deny Bash(rm *) with saída: 1 in executar.json.
Run the real block (one call, uses your subscription quota).
Done when: build/app.bin continua lá: deny segurou and saída: 0; saída: 3 is an empty reply (try again later), not a result.
Check whether your config.toml turns the sandbox off for all calls. It's common for it to stay on danger-full-access after a workaround (for example, when bubblewrap failed) and never go back; in that case, every codex exec in the loop needs an explicit -s.
Done when: grep -n sandbox_mode ~/.codex/config.toml prints nothing or prints read-only/workspace-write; if it prints danger-full-access, every codex exec in your loop takes -s.
Question 1 of 4
You have deny Bash(git push *) and allow Bash(git push origin feature *). What happens with git push origin feature-x?skill
Question 2 of 4
Why doesn't Bash(rm -rf *) block rm -r -f build?skill
Question 3 of 4
Which Codex -s for the step that edits the project with no network?
Question 4 of 4
In the loop with --permission-mode dontAsk, a command that matches no rule…
Choose an option. The explanation appears after your attempt.