In "Why AI agents should stop asking for permission" I picked a fight with the permission prompt. That post was the punch. This one is the engineering behind it, because "stop asking" only works if something better is standing where the prompt used to be.

The lazy version of an autonomous agent is easy to build: give the model a shell, turn off the confirmations, and pray. I have no interest in that, and neither should you. The question I actually care about is harder: how does an agent act on its own, for minutes or hours, while the person stays the final authority over what happens on their machine?

My answer, after a lot of rewriting in the ClarkCant codebase, is that authority is not a dialog box. It is a set of things the person holds: the policy, the ability to stop, the record of what happened, the right to settle anything uncertain, and the limits nothing can exceed. Here is each one, where it lives in the code, and where it is still weak.

The final authority is a role, not a prompt

Start with who is allowed to decide. In ClarkCant only a user principal can approve an operation; that is enforced in the grant contracts, not in the UI. The open interfaces make it sharper: there is deliberately no approval tool on MCP. An AI client that could call "approve" could approve its own guarded action, so every route that records a person's decision answers 403 PERSON_ONLY when it arrives over MCP or the generic relays.

So the person is not asked about everything. But the things only a person may do, approving, deciding an install, trusting a paired machine, issuing a grant, reconciling an unknown outcome, cannot be done by anything else. That is what "final authority" means in practice.

The path every effect takes in ClarkCant. Your request goes to host preflight, which checks the folder is owned and attaches a deadline and output cap. If valid, the execution policy decides to deny, ask or run. If it asks, the person decides and an approval leads to execution. If it allows, the Jev guardrail may allow or narrow, never widen, and then the effect runs with secrets injected just in time. Execution produces evidence and an append-only audit entry. If the answer is lost, the outcome is unknown, the task becomes uncertain, and only a person can settle it. Stop kills the process group of running work at any point.

Policy: one decision, declared once

Until 19 September 2026, every seam in ClarkCant decided for itself whether to ask. Now there is one function, decideExecution in packages/core/src/execution-policy.ts, and every effect goes through it: a command, a widget action that is not just a view change, an install. Its order is written at the top of the file: prohibition, then hard boundary, then rules, then mode.

A node-wide "deny everything" beats all. Hard boundaries come next: an OS permission, an OAuth grant, a browser device permission or a vendor's own consent screen is asked for in every mode, because it is not this application's decision to make. Then your rules per category of effect, where "deny" always wins. Then the mode: Autonomous (the default), Guarded, Ask every time.

Autonomous has a risk gate. If the agent decides on its own to write outside the machine, destroy something, spend money, communicate or capture audio or video, it asks, because no person's instruction covers it. Work handed over by a paired node runs only within what this node's owner allowed that peer. And a mode the build does not recognise fails closed: the honest answer to "may I?" is no, not a guess.

Bounded execution and isolation

Policy decides who is asked. Preflight decides what is possible, and it runs before any model is consulted. In apps/runtime/src/preflight.ts every command must resolve inside a folder the node owns, the folder must exist, and the operation carries its budget: two minutes and 8,000 bytes of output per stream for run_command. A capability the node has not discovered cannot be invoked, whatever the proposal claims.

Jev, the guardrail, sits after that and is explicitly a judgment layer, not a security boundary. It can allow, deny, ask a clarifying question, or narrow. If its answer would widen anything, a longer deadline, a bigger output, a directory outside the one the host resolved, applyGuardrailConstraints refuses the whole answer rather than quietly clamping it.

Isolation is concrete rather than aspirational. A child process inherits an allowlisted environment, so SSH_AUTH_SOCK, AWS_* variables, GITHUB_TOKEN and provider keys are dropped. Every child runs in its own process group so a stop reaches grandchildren. A dispatched task worker has no shell at all: its commands cross an IPC channel to the host and go through the same preflight, policy, guardrail and audit, and where that path would need an approval card, the broker refuses instead, because a background task should never wait on a question nobody is looking at. A browser task gets its own Playwright profile under the node's data directory, never your browser, limited to the sites you named, with private-network addresses refused and the profile deleted when the run ends.

The docs are honest that none of this is a perfect sandbox. They say outright not to advertise a lease as one, and that a browser context is not a security boundary.

Source excerpt from apps/runtime/src/preflight.ts, lines 436 to 452, the start of applyGuardrailConstraints. For each constraint the guardrail returns, a timeout longer than the one the host set is refused with GUARDRAIL_WIDENS, the message saying the guardrail asked for more time and may only narrow; a shorter timeout is applied. The same pattern follows for the output ceiling and the working directory.

Resource boundaries

An agent that can run unattended needs limits that do not depend on its good behaviour. Background requests run at most 3 at a time by default (1, 3 or 5 in Settings), with a queue of 10, and the main conversation never counts against that. A task worker that sets no budget gets 500,000 tokens and 10 minutes, configurable with CC_WORKER_MAX_TOKENS and CC_WORKER_MAX_WALL_CLOCK_MS. When the time runs out the worker is stopped and the task says so.

One honest caveat from the docs: tokens are counted when a turn ends, so a worker can overshoot its token budget by up to one turn. Between machines, budgets are intersected like everything else. A grant from a paired node is only ever narrowed by holding another grant, and a grant that does not set a byte budget for returned files allows zero bytes, not unlimited.

Default limits in ClarkCant: 3 background requests at once (1, 3 or 5 in Settings), a background queue of 10, a worker token budget of 500,000 tokens (CC_WORKER_MAX_TOKENS), a worker time budget of 10 minutes (CC_WORKER_MAX_WALL_CLOCK_MS), a 2 minute deadline for run_command and 8,000 bytes of output per stream.

Underlying data
[
  {
    "id": "bg",
    "label": "Background requests at once",
    "value": 3,
    "hint": "1, 3 or 5 in Settings"
  },
  {
    "id": "queue",
    "label": "Background queue",
    "value": 10
  },
  {
    "id": "tokens",
    "label": "Worker token budget",
    "value": 500000,
    "unit": "tokens",
    "hint": "CC_WORKER_MAX_TOKENS"
  },
  {
    "id": "wall",
    "label": "Worker time budget",
    "value": 10,
    "unit": "min",
    "hint": "CC_WORKER_MAX_WALL_CLOCK_MS"
  },
  {
    "id": "cmd",
    "label": "run_command deadline",
    "value": 2,
    "unit": "min"
  },
  {
    "id": "out",
    "label": "run_command output per stream",
    "value": 8000,
    "unit": "bytes"
  }
]

Provenance: who asked, and where this came from

Autonomy turns on one question: whose intent is this? ClarkCant records it on every task. Interactive means a person asked in the conversation. Delegated means a paired node handed it over under a grant. Persistent means an automation a person set up earlier, limited to the effects they gave it. System means the node's own work, which nobody asked for, and which the policy treats accordingly.

Within interactive turns, the node also records which surface the message came from: the person, an AI client over MCP, a relay, a program on the HTTP API. By default a program is treated like you, and the Settings copy is frank that a program holding the node token could change that setting anyway. If you opt in, risky effects asked for by a program wait for you.

Provenance also covers what goes into the model. Project instructions from a .clarkcant folder are framed as data that grants nothing. Page text from a browser task reaches the model as data, never as instructions. A signal from a paired node is recorded under the sender the authenticated channel names, not the one the message claims. And the design rule for widgets is that nothing claims "live" when it is a snapshot or a sample.

Audit: shallow on purpose

The audit trail in packages/storage/src/audit.ts answers the question you ask after something surprising: what ran, when, and how it ended. Nothing in that module updates or deletes a row. It records commands, each use of a secret by name and consumer, approvals, stops, which model a worker was started on and where its key came from, and more. It never records a value. The comment puts it plainly: a secret that appears in the audit is a secret that has leaked.

It also refuses to blur outcomes. A stopped command is recorded as stopped, not failed, because a trail that cannot tell them apart misleads whoever reads it later. In the interface, Settings has an "Effects run without asking" list, which is where Autonomous mode owes you a trace.

Reversible where it is honest

I would love to say everything has Undo. It does not, and the design document forbids promising Undo for irreversible external actions. What exists is real. Every preference write keeps the value it replaced, so undoing returns to what was actually there, including deleting the row if there was nothing before. A dismissed inbox notice can be restored for five minutes, after which the server answers UNDO_EXPIRED rather than pretending. A package install creates immutable generations, and rollback leaves the previous generation serving.

Revoking a paired machine blocks its future operations. It does not roll back effects that already happened, and the docs say exactly that.

Uncertainty is a resting state

This is the part I am proudest of, and it is the least flashy. Every external effect goes through a ledger: prepared, submitted, then confirmed, failed or unknown. If the acknowledgement is lost, the effect is unknown, and unknown is never retried into success or failure. The task moves to uncertain, the inbox gets one notice quoting what happened, and only the person can settle it. In a browser task, while a submit is unknown, no further click of that task reaches the page, so the button is never pressed twice.

Success has the same discipline. The only path into succeeded is verification with recorded evidence: an exit status, a file version, an API receipt, observed browser state. A worker going idle, a model ending its turn or a socket closing is not success. The architecture doc even says: if there is no test count, do not invent a test count. That is what I mean when I say Clark should be able to say "I don't know". It is not a personality trait. It is a state in the state machine.

Source excerpt from packages/contracts/src/effects.ts, lines 70 to 77. Allowed effect transitions: prepared can become submitted or failed; submitted can become confirmed, failed or unknown; unknown, reached only by observing the external system, can become confirmed or failed; confirmed and failed are final.

Recovery

Machines crash and laptops close. After a restart, apps/runtime/src/work-recovery.ts reports every unfinished run once, in the conversation where it was asked for. An orphaned process is stopped only when the machine boot and the kernel's start time for that pid match what was recorded, so a recycled pid is never touched. Background work with no effects is re-run once, only in Autonomous mode and only within a day. Work with effects is never re-run: it becomes uncertain and waits for a person.

Stop is the other half. POST /stop kills the process group of every running command, worker and terminal, interrupts turns and cancels queued background work. You can also just ask Clark to list or stop work. A peer that has not answered for more than 10 minutes gets one notice that says what is owed and what to do, not a spinner.

A map of controls to their location in the ClarkCant codebase and their status. Execution policy: packages/core/src/execution-policy.ts, built, default mode Autonomous. Host preflight: apps/runtime/src/preflight.ts, built. Jev guardrail, narrow only: preflight.ts and jev-decider.ts, built, fails open by default. Secret broker: apps/runtime/src/secret-broker.ts, built, OS keychain backend later. Child environment allowlist: apps/runtime/src/child-env.ts, built. Append-only audit trail: packages/storage/src/audit.ts, built. Stop: POST /stop and work-supervisor.ts, built. Undo and rollback: preferences.ts and install-lifecycle.ts, partial, covering preferences, notices and package generations. Effect ledger with unknown: packages/contracts/src/effects.ts, built. Task states with evidence-only success: packages/contracts/src/tasks.ts, built. Recovery after restart: work-recovery.ts, built. Worker and background budgets: env-file.ts and work-supervisor.ts, built. Grants intersect between nodes: packages/contracts/src/grants.ts, built, but the peer channel has no TLS yet.

Underlying data
[
  {
    "id": "policy",
    "control": "Execution policy",
    "where": "packages/core/src/execution-policy.ts (decideExecution)",
    "status": "Built. Default mode Autonomous"
  },
  {
    "id": "preflight",
    "control": "Host preflight: owned folders, existence, deadline, output ceiling",
    "where": "apps/runtime/src/preflight.ts",
    "status": "Built"
  },
  {
    "id": "guardrail",
    "control": "Jev guardrail, narrow only",
    "where": "applyGuardrailConstraints in preflight.ts, jev-decider.ts",
    "status": "Built. Fails open by default"
  },
  {
    "id": "secrets",
    "control": "Secret broker, secrets as metadata",
    "where": "apps/runtime/src/secret-broker.ts",
    "status": "Built. OS keychain backend later"
  },
  {
    "id": "env",
    "control": "Child environment allowlist",
    "where": "apps/runtime/src/child-env.ts",
    "status": "Built"
  },
  {
    "id": "audit",
    "control": "Append-only audit trail",
    "where": "packages/storage/src/audit.ts (audit_log)",
    "status": "Built"
  },
  {
    "id": "stop",
    "control": "Stop for all running work",
    "where": "POST /stop, apps/runtime/src/work-supervisor.ts",
    "status": "Built"
  },
  {
    "id": "undo",
    "control": "Undo and rollback",
    "where": "packages/core/src/preferences.ts, install-lifecycle.ts",
    "status": "Partial: preferences, notices, package generations"
  },
  {
    "id": "effects",
    "control": "Effect ledger with unknown",
    "where": "packages/contracts/src/effects.ts",
    "status": "Built"
  },
  {
    "id": "tasks",
    "control": "Task states, success only with evidence",
    "where": "packages/contracts/src/tasks.ts",
    "status": "Built"
  },
  {
    "id": "recovery",
    "control": "Recovery after restart",
    "where": "apps/runtime/src/work-recovery.ts",
    "status": "Built"
  },
  {
    "id": "budgets",
    "control": "Worker and background budgets",
    "where": "packages/pi-adapter/src/env-file.ts, work-supervisor.ts",
    "status": "Built"
  },
  {
    "id": "grants",
    "control": "Grants intersect between nodes",
    "where": "packages/contracts/src/grants.ts",
    "status": "Built. Peer channel has no TLS yet"
  }
]

What this still is not

ClarkCant is a bootstrap, not a release, and the conformance ledger in the repository marks nothing complete. Some specifics. The guardrail fails open by default when it cannot be reached; preflight and containment remain, and you can switch it to stop. The link between paired nodes has no TLS yet: key verification is a fingerprint comparison over plain HTTP. Epoch fencing at the point an effect is submitted is not wired yet. Browser tasks have no human takeover or live preview yet, and Computer Use is partial, waiting on a signed native binding. Prompt injection can still steer a model; isolation limits the damage, it does not prove safety.

I would rather publish that list than have you find it.

Status board of ClarkCant controls as documented in the repository. Built and tested: one execution policy for every effect, host preflight and containment, the append-only audit trail, Stop for all running work, and unknown outcomes with uncertain tasks. Partial: Undo and rollback (preferences, notice dismissal and package generations, not external actions), guardrail availability (fails open by default while preflight still holds), and Computer Use (waits on a signed native binding). Not yet: TLS between paired nodes (key check is a fingerprint comparison over plain HTTP), epoch fencing at submit, and human takeover or live preview for browser tasks.

Control is what you can do afterwards

The permission prompt gives you the feeling of control before the action. What I want is control that holds during and after it: a policy you chose, boundaries a model cannot argue with, a record you can read, a Stop that actually stops, an Undo that never lies, and an agent that says "unknown" instead of "done" when it does not know.

That is not a YOLO agent. It is closer to a good colleague: you hand over the job, they get on with it, and when something is outside what you asked or they are not sure what happened, they come back to you. I might have the balance wrong in places. Tell me which control you would need before you let an agent run while you sleep.

Which control would you need most before letting an agent run unattended?