Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Permission modes for coding agents

After this lesson: I can say which actions an agent may take without asking in plan, ask and auto modes.

65 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

One agent, three levels of trust

A permission mode decides what an agent may do without asking. In plan mode it only reads. In ask mode it asks before it changes anything. In auto mode it works alone inside limits.

The same agent is safe to leave running in one mode and unsafe in another. So the mode is something you choose per run.

By the end of this lesson

Note

I can say which actions an agent may take without asking in plan, ask and auto modes.

Key words

WordWhat it means
Modea setting that decides what runs without asking
Plan modea mode that only reads
Riskhow much harm an action can do
Allow listcommands approved in advance
Protected patha file that always needs approval
Approvalthe user's yes or no to one action

The cells students fill in are left blank in this preview.

A mode is a setting, not a prompt

You could write "never change files" in the prompt. A model may still do it, because a prompt is a request.

A mode is code in the harness. Every action passes through one function before it runs. In plan mode that function refuses a write, whatever the model says. The model never sees a way round it.

Rules are checked in order

Give each action a risk: read, write, run or destructive. Then decide by mode, and put the rules in a fixed order. Plan mode comes first, because it refuses more than any other.

Next, a protected path or a destructive action always asks. Only after that does a mode relax anything. An allow list lets auto mode run named commands without asking.

What each mode does with a risk

RiskPlanAskAuto
ReadAllowAllowAllow
WriteDenyAskAllow
RunDenyAskAllow if listed
DestructiveDenyAskAsk
Protected pathDenyAskAsk

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people ask for approval on every action. People then click yes without reading. Ask only where it matters: protected paths, destructive actions and commands off the list.

Write the model helper, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' '  return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the model helper, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    const data = await res.json();' '    const text = data.choices[0].message.content;' '    return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    return { text: sample, inTokens: null, outTokens: null, recorded: true };' '  }' '}' 'module.exports = { ask };' >> model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the mode checker, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const ALLOWED = [\"pytest\"];' 'const actions = [' '  { tool: \"read_file\", risk: \"read\", target: \"src/app.py\" },' '  { tool: \"write_file\", risk: \"write\", target: \"src/app.py\" },' '  { tool: \"run\", risk: \"run\", target: \"pytest\" },' '  { tool: \"run\", risk: \"run\", target: \"curl site\" },' '  { tool: \"delete_file\", risk: \"destructive\", target: \"src/old.py\" },' '  { tool: \"read_file\", risk: \"read\", target: \".env\" },' '];' > modes3.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the mode checker, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function protectedPath(t) {' '  return t === \".env\" || t.startsWith(\".env.\") || t.startsWith(\".git/\") || t.toLowerCase().includes(\"secret\");' '}' 'function decide(mode, a) {' '  if (mode === \"plan\") return a.risk === \"read\" && protectedPath(a.target) === false ? \"allow\" : \"deny\";' '  if (protectedPath(a.target) || a.risk === \"destructive\") return \"ask\";' '  if (mode === \"ask\") return a.risk === \"read\" ? \"allow\" : \"ask\";' '  if (a.risk === \"run\") return ALLOWED.includes(a.target) ? \"allow\" : \"ask\";' '  return \"allow\";' '}' >> modes3.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the mode checker, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  for (const a of actions) {' '    const row = [\"ask\", \"plan\", \"auto\"].map(function (m) { return m + \":\" + decide(m, a); });' '    console.log(a.tool + \" \" + a.target + \" -> \" + row.join(\" \"));' '  }' '  const q = \"An agent wants to delete src/old.py. Write one short sentence that asks the user to approve it.\";' '  const reply = await ask([{ role: \"user\", content: q }], \"The agent wants to delete src/old.py. Do you allow it?\", 40);' '  console.log(\"question shown: \" + reply.text);' '}' 'main();' >> modes3.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the actions in three modes

Command
docker exec -w /home/node/project agent node modes3.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

The decisions need no model

The six lines come from the decision function alone, so they are the same for every student. The Run then makes one short call, to word the question a user would see before a delete.

With no model it prints a Recorded sample for that one line.

One possible output

LineWhat it shows
read_file src/app.py -> ask:allow plan:allow auto:allowA plain read passes everywhere
write_file src/app.py -> ask:ask plan:deny auto:allowOnly auto lets a write through
run pytest -> ask:ask plan:deny auto:allowIt is on the allow list
run curl site -> ask:ask plan:deny auto:askNot on the list, so auto asks
delete_file src/old.py -> ask:ask plan:deny auto:askA delete always asks
read_file .env -> ask:ask plan:deny auto:askA protected path always asks

The cells students fill in are left blank in this preview.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.