The six permission tiers
Nothing runs until you say what it may do. Tiers are granted separately, per device and per workspace.
The tiers
| read | Read files in the workspace. Almost every task needs it. |
| write | Create and edit files in the workspace. |
| exec | Run commands — builds, tests, formatters, package scripts. |
| web | Search or fetch from the internet. |
| push | Push to a remote, fetch, pull. |
| danger | Delete or migrate data. Never granted remotely. |
Granting
Grants are recorded for that device in that workspace. There is no global switch.
Where a grant is checked
A grant is checked where the action happens, not written into a prompt asking the model to behave. The runtime's writes, edits and commands go through KernelHub's built-in Tool Server, which checks the grant and the workspace boundary before anything runs; commands also pass the Executor's exec rules, and every action is recorded. Where nothing else can prove a tier is held, the runtime is marked NOT_READY rather than counted as enforced.
What no grant can lift
Credential paths — SSH keys, cloud configuration, .env files, keychains — and high-risk operations are denied independently of what you granted. That list does not shrink when you approve a tier.
Task-scoped approval
When a task needs a tier it does not have, it raises a card. Approving applies to that task only, in that workspace, and expires the moment the task reaches a terminal state.
Delete / migrate
This tier cannot be granted from a phone. Where a task would cross that boundary and the agent cannot express a deny list, KernelHub stops before doing anything and asks you to release that single run. Approving that card grants no permission tier — the deny list stays exactly as it was.
Blocked is not the same as failed
Two different things can end a command, and KernelHub keeps them apart in the machine-readable record, not just in prose:
| Blocked | The permission system refused to run it. Nothing happened on disk. It appears in blocked_actions. |
| Failed | It ran, and it came back non-zero — a test failed, a build broke. That is the employee’s job to handle, not something we refused. |
The difference is taken from the execution side’s own decision record — never from an exit code and never from the wording of an error message. A go test that exits non-zero is not a blocked action.
Three verdicts, and one that is not a verdict
Every decision is exactly one of three. Merging any two of them loses a distinction you can see on screen.
allow | Already granted. It runs. |
need_grant | One tier is missing, and that tier can be approved from your phone. A card is raised and the task waits. Say yes and the same run continues — nothing restarts. |
never_granted | This class is never granted for a long time. There is no card on your phone — not because we forgot to show one, but because the only route is a one-off approval on that machine (icloser approval). Credentials and delete/migrate live here. |
scope_violation is not a fourth verdict — it is a separate flag that says why: the path resolved to somewhere outside this task’s workspace. It answers a different question from "which tier is missing", so it gets its own field. No tier fixes it. Granting all six changes nothing; workspaces do not reach into each other. If you need work done in another project, send the task to that workspace.
Why these are separate fields rather than something you read out of the message text: wording changes, and when it does, anything matching on wording stops matching — silently.