Permission modes
# grade this stage (from your course directory)
npx theinference test --stage modes --watchYou can match a rule. Now turn that into a verdict. decide(settings, call) returns allow | ask | deny by walking the precedence you learned in step 01: check deny, then ask, then allow, and if no rule matched, fall through to the active mode's default. This one function is the heart of the permission engine — step 04 adds protected paths and step 05 wires it into the loop.
What to build
Implement decide
Implement decide(settings, call) in src/permissions.ts. In order: if any settings.deny rule matchRules the call, return 'deny'; else if any settings.ask rule matches, return 'ask'; else if any settings.allow rule matches, return 'allow'; else apply the mode default for settings.defaultMode (default 'default'). The mode default: read-only tools (Read/View/Glob/Grep) → 'allow'; then bypassPermissions → 'allow'; plan → 'deny' (side effects blocked); acceptEdits → 'allow' for write tools (Write/Edit), else 'ask'; default → 'ask' on any write or bash. The tests/adapters.ts adapter forwards to it.
import { decide, type PermissionSettings, type ToolCall, type Decision } from '../src/permissions'
export function runDecide(settings: PermissionSettings, call: ToolCall): Decision {
return decide(settings, call)
}
mode read-only write/edit bash/side-effect
default allow ask ask
acceptEdits allow allow ask
plan allow deny deny
bypassPermissions allow allow allow
Process mode runs program.ts decide over a {settings, calls[]} matrix and compares the allow|ask|deny vector to expected.json. The matrix crosses each mode with read/write/bash calls and with deny/ask/allow rules present — including the override traps: a deny rule under bypassPermissions still denies; an allow rule under plan still allows. Every cell must match the reference.