Lesson 5 of 6
Watch permissions fire; do not confuse cost reports with caps
Configure allow/ask/deny so denied actions are refused, test edits ask, secrets stay out of the repo, and you can tell a hard limit from a cost report you read after the run.
Allow, ask, deny — and last match wins
Every permission rule is allow (run), ask (pause), or deny (block). OpenCode starts permissive: most tools default to allow. doom_loop and external_directory default to ask. .env reads default to deny (*.env, *.env.*; .env.example allowed). If you set read to a bare "allow", you replace that object and can lose the .env deny — keep the object form.
Use pattern objects for edit and bash. Last matching rule wins, so write "*" first:
"bash": { "*": "ask", "npm test*": "allow", "git push*": "deny", "rm *": "deny" }
Match the whole command when there are arguments ("grep *" not a bare "grep"). When OpenCode asks, once is this call, always is the suggested pattern for the rest of the session, reject is deny. Read the pattern before you choose always.
The complete ready-to-use rule file is opencode.json in the starter; this small block belongs under its permission object, not at the JSON root. Keep its .env read denies and the protected config/test patterns intact.
Hard limits versus reports you read afterwards
Do not say “cost is capped” unless a control actually stops the run.
Hard (the harness stops or blocks): deny rules; agent.build.steps (iteration cap, then a summary prompt); provider timeout / headerTimeout / chunkTimeout (request timing, default 300000 ms, not a spend cap).
Not hard: opencode stats prints token usage and cost after sessions — it does not kill a live run. A “30 minutes” line in the brief is a bound you enforce. --auto (and opencode run --auto) auto-approves everything not denied; it does not cap cost or steps. A larger context window is not a budget.
Write the wall-clock budget in the brief and stop when it is reached. Then run opencode stats and copy the figure into the log as an observation, not as proof that spend could not have gone higher.
Secrets stay out of the project and out of the prompt
This tracker needs zero app secrets. Do not add an API key to “make persistence real”; that would be a new server requirement.
Use a supported provider connection you already have, as in build-and-launch Lesson 1. An eligible LintLabs key uses /connect → OpenRouter; another provider uses its supported authentication flow. Free lessons do not include API credits. Check current pricing and spending limits before running the agent. Keep credentials in OpenCode's authentication store, outside the repo. Never paste the value into opencode.json or into evidence. Keep .env* gitignored. Anything you paste into the session is in the transcript.
If a secret is committed or pasted, revoke or rotate it at the account that issued it, then reconnect OpenCode. For a LintLabs-issued key, use Rotate key on /settings. Deleting the file does not un-leak the value. For this module, the probe is the opposite: confirm no key appears in git status, in config, or in the run log you will submit.
Probe the gates; do not trust the file unread
A rule you have not seen fire is an assumption. After opencode debug config shows the resolved object, create a .env containing only LAB_PLACEHOLDER=not-a-secret and verify it is gitignored. Ask the agent to: (1) read that placeholder file, (2) request git push --dry-run, (3) edit a test file. Do not approve any unexpected request. Record refused / asked / allowed for each.
Expected with this module’s posture: .env read denied; git push denied; test edit asked. Also deny webfetch, websearch, and external_directory so the agent is not an arbitrary URL crawler and stays in the workspace.
Do not enable opencode --auto while practising this. Do not run opencode github install here: that GitHub bot is a real product feature and is out of scope; checks are tests, and the reviewer is you.