Autonomous Loop Security|INEMA.CLUB-PRO

Lesson 4 · 20 min

Permissions per step: capabilities and policy

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
A board of colored access cards hanging on hooks, some empty, next to a card reader.

In this lesson you will

  1. 1deny, then ask, then allow: the fixed order from the docs
  2. 2Why Bash(rm -rf *) doesn't catch rm -r -f
  3. 3One settings.json per step and the right -s in Codex
  4. 4A cap per type of action: the inema-agent-runtime policy

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.

01One permission per step

Click each step.

Click each block in the diagram to see what it does.

An "always allow" permission never goes past the cap of the action (POLITICA.md of inema-agent-runtime).

02The cap on each action

From the POLITICA.md of inema-agent-runtime (10/05/2026), summarized.

1. Read: N4

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.

03The settings.json for the execute step

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.

File · kit/permissoes/executar.json
{
  "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/**)"
    ]
  }
}
Template · one line per step (Claude Code and Codex)
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

04Rehearsal: which rule decides each command

regra.py imitates, in simplified form, what the docs describe (order, subcommands, $( ), the trailing *). The one that really decides is Claude Code.

Terminal · narrow deny and right deny
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
Example from the repository
## 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).

05Real: deny holds even in the mode that allows everything

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.

Terminal · one real call
bash ~/projetos/loop-r-eng-seguranca-v72/kit/permissoes/real.sh; echo "saída: $?"
Example from the repository
build/app.bin continua lá: deny segurou (código do claude: 0)
saída: 0

06Practice

The rules, the real call, and your Codex sandbox.

E9 · Where the narrow deny lets things through

10 min

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.

E10 · The deny in a real session

5 min

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.

E11 · Your Codex's default sandbox

5 min

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.

07Lesson quiz

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

08In three sentences

Each step, its ownPlanning and reviewing do not edit; executing edits only where it needs to.
deny always winsdeny → ask → allow, and subcommand counts. Write Bash(rm *), not just rm -rf.
explicit -sIn Codex, the sandbox goes on the loop line, not in the hope of the config.

Going further

Previous lessonNext lesson