Permissions
Every automation declares what it may do in a comment header at the top of the file, and Tranquil runs it with exactly that and nothing more. This page lists the grants, says how they are enforced, and covers approval and timeouts.
The header
The declaration goes in the leading comment block, above the imports:
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions forms sites=jobs.example.com read=profile.json
// @timeout 15m
import { tabs } from "tranquil/automation"; A header is required. A script with no @permissions line is refused rather than run on a
silent baseline, because “declares nothing” and “needs nothing” shouldn’t look the same. A script
that touches nothing outside itself says so explicitly:
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions none
console.log("Hello from a script with no grants."); The sandbox is default-deny: files (including the ones next to the script), the browser, the clipboard, the network and running other programs are all denied unless declared.
The grants
Grants are space-separated on the one @permissions line. The browser grants form a ladder, where
each includes the one above it; ask for the lowest one that does the job:
flowchart TD
subgraph BROWSER["browser: all of these"]
TABS["tabs<br/>open tabs, read pages"]
FORMS["forms<br/>type, choose, click, upload"]
SUBMIT["submit<br/>also submit a form"]
PJS["page-js<br/>run your own code in a page"]
end
TABS --> FORMS
FORMS --> SUBMIT
TABS --> PJSAll the grants:
| Key | Grants |
|---|---|
none | Nothing at all: no files, no browser, no network. |
tabs | Open, find, navigate and close tabs, and read pages: fields, clickables, text, attr, waitFor. |
forms | Everything in tabs, plus typing, choosing, clicking and uploading into pages, but never submitting a form. |
submit | Everything in forms, plus submitting a form: a submit button, or Enter in a field. |
page-js | Everything in tabs, plus running the script’s own code inside pages with evaluate, as you. |
browser | All of the above. |
sites=host[,host] | The only sites the script may act on: sites=jobs.example.com,*.example.org (a *. pattern also covers the bare domain). |
clipboard | Reading and writing the system clipboard. |
net=host[,host] | Network access to those hosts. |
run=cmd[,cmd] | Running those commands: effectively full user privilege, so grant deliberately. |
read=path / write=path | File access, by exact path. Relative paths resolve against the script’s folder (write=output.md, write=results). A bare . is rejected. |
env=NAME | Reading those environment variables. |
import=host | Importing modules from hosts beyond the defaults. |
jsr: imports are allowed without a grant, so the Deno standard library needs none.
How they are enforced
The browser, the clipboard and file, network and command access each reach the script a different way, so they are enforced in different places:
- Deno flags. Files, network, commands and environment variables become Deno’s own
--allow-*flags, scoped to exactly the declared paths, hosts and names. The sandbox refuses anything else. - The SDK’s grant check. Before every SDK call, the SDK checks that the header granted it, and
names the missing key when it didn’t:
fillwithoutforms,evaluatewithoutpage-js. sites=. Every page action reads the page’s address first and refuses with anot_permittederror on any other site. A form inside an iframe counts by the frame’s own site.submit. Without it, a click or key that would submit a form is stopped inside the page and refused, and a click on a button named like Submit, Send, Pay or Place order is refused before it happens. With it, the same applies until you say yes to the run’s most recentui.confirm, so a script that asks and then submits after a no is refused.
The checks on the browser grants bind scripts that use the SDK. A script granted any browser key can also reach the browser’s debugging port directly, which a later change will close; see ADR-0028 and The guard rails in this flow.
File grants are paths
File grants are paths, not a blanket. Deno’s path permissions are recursive, so a directory grant
covers everything beneath it, including folders that do not exist yet. A bare . is therefore
rejected: it would hand the script its whole folder, which is exactly the implicit grant this model
removed, only written down. Name the file you write (write=output.md), or a subfolder you own
(write=results).
Approval
The first time you run a script that asks for anything, Tranquil shows you what it declared and waits for you to approve it: the grants, the sites it is limited to (or that it has no limit), and whether it may submit forms. When its page code reaches sensitive web APIs (requests made as you, cookies and storage, navigation, the clipboard), the prompt lists those too.
- Remembered per script. You’re asked once, not on every run.
- Re-asked on change. Editing the header asks again, so a script can’t quietly widen what it may do after you approved it.
nonenever asks.
Declining isn’t a failure. The run is recorded as cancelled in the Automation Runs panel, and a refused run (a missing or malformed header) is recorded there too, with the reason, so the message is still there after the notification goes away.
Timeouts
@timeout bounds the run: 90s, 15m, 2h, or none for a watcher-style script. The default is
10 minutes.
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions tabs
// @timeout 2m
import { tabs } from "tranquil/automation";
const tab = await tabs.active();
console.log(await tab.title());