Security
A fetched reflex runs as you, held to what its manifest declares. evoke add and evoke update are the trust
decisions. Everything below is what the tool guarantees around them, and what it does not.
What runs, and as whom
- A body runs with your user, under a scrubbed environment of five variables:
PATH,HOME,TMPDIR,LANG,TERM.TMPDIRis a private folder, made for the run and removed after it. The body receives secrets only through its config, for one run. The scrubbing prevents leaks. Read what you install, and its declaration, the next point, with it. - A body is held to its declaration.
[needs]in the manifest names the paths the body reads and writes, whether it reaches the network, and the programs it runs: What the body touches. Leaving the table out is the tightest declaration: the body's own directory,TMPDIR, and nothing else. Two layers hold the body there. The kernel's: on Linux a Landlock ruleset,no_new_privsand a seccomp filter that closes the network, applied to the process before the body starts; on macOS Seatbelt, throughsandbox-exec, with a profile that denies everything the declaration does not name. And, for a file body, Node's permission model, which refuses a read past the declaration with the path before the kernel is asked. A program named inrunsruns under the kernel's layer too, so it reaches no further than the body; what it asks the system to do — Finder emptying the trash, the login window restarting — happens outside the declaration, soruns osascriptis read beside the effect.evoke checkimports a body under the same hold, with nothing configured. - A reach past the declaration ends the run. The line names what was reached and the key:
~/secret.txt is not in [needs] reads,example.com is not in [needs] hosts,[needs] runs names no program. Its fix depends on where the declaration is written: the manifest's line for a local reflex; for a fetched one,evoke update --acceptwhen upstream's own declaration already allows it, elseevoke remove. The run exits 1, a failure. - The status is printed, never assumed. A machine that holds only part of a declaration, or none of it — a
Linux kernel without Landlock or before its third version, a Mac without
sandbox-exec— still runs the body, and says so: one line ataddand atshow,not contained <why>;· partly containedor· not containedat the end of the lineevokeprints before every body runs; andcontainedin--json. - No code runs at install time.
addfetches a tree, reads a manifest, lints it, and writes files. A body runs only at a decision, atevoke run, or whenevoke checkimports it to see that it exports a function. - A reflex handed to the SDK as code is your application's own. It runs in its process, unscrubbed and unheld.
It must honour its
signal, since nothing can end it from outside. A reflex the SDK loads from a project runs held as far as the package can: under Node's permission model on both systems, and under Seatbelt on macOS. On Linux the kernel's layer needs native code the package does not carry, so a file body runs partly held and an argv body unheld there, and the result'scontainedsays which.
What the classifier cannot do
- State is only
{ request }. The adapter sees the sentence and the questions built from installed manifests. Nothing else of yours. - Arguments are closed sets, exact spans, or an earlier step's result. A value is an author's option key, one of your vocabulary words, a piece of the input read by a recognizer and checked against its range, or, in a sentence of several steps, a field of an earlier step's result or that step's whole result, bound by the plan and shown on its line before anything runs; or, at a prompt, a value a body of this session returned under a field the author named, listed for you to choose and read by the recognizer. Injected text can choose a call. It can never mint a value. A date reaches a body as a day of your machine's calendar, never as a word.
- The effect gate covers what it chooses. A destructive reflex always confirms, and there is no
--yes. Unattended use is a threshold in a file you own and trust. Areadorwritereflex over its floor runs without asking. Whatever it does with a span it is handed, like a URL or a quoted text, an injected sentence can make it do. So its effect is the author's promise about exactly that. Awritereflex that takes an earlier step's whole result runs with it over its floor, unasked; the plan printstakeson its line first, and a destructive reflex takes none. - Prompts cannot be repainted. Control characters and bidi overrides are refused in every manifest, overlay
and vocabulary string, and in every span.
evoke's own line, with the call, the effect and the weakest judgment, prints before the reflex's template. - An input over 2 000 characters is refused, and one decision has 30 seconds.
What a manifest cannot do
- Wording is linted and conflict-tested at
add. Text that addresses a model is named. Every phrase a newcomer would steal from an installed reflex is reported, with its one-line fix. - Effect and needs only tighten. The lock holds the effect and the declaration you consented to. Upstream
may tighten either at any tag. Loosening waits for
evoke update --accept, which says what widened. Your overlay may tighten the effect, never loosen it, and cannot touch[needs]. - The contract cannot be overridden. An overlay that tries makes the reflex inactive, not looser.
- Tags are only for
--tag. No label, category or claim in a manifest changes what may run.
What the files guarantee
- Trust binds to content. A project outside home decides only after
evoke trust. The digest of its four owned paths is checked at every run. Agit pullthat changed an overlay is a stop, not a surprise. A local reflex,./dir, is outside those paths. Its directory is yours, so a pull that changes it runs what it now holds, under the effect its manifest now claims. - The lock binds to content. Every remote reflex is pinned by tag, commit and
h1. The store's copy is hashed again every timeevokestarts. A tag that moved is refused. - Fetching is narrow.
gitis called with a strict ref grammar,--end-of-options, and an allow-list ofhttpsandssh. Symlinks and submodules are rejected. There is no checkout, no hook, and no clone-time script. - Secrets are referenced, never stored.
evoke.tomlnames a variable. The value exists in your environment, and reaches a body for the length of one run.evokewrites no key and no config value under the cache or the state directory. The log and the REPL's history hold what you typed and what a body returned. Only you can read them. - Adapter names resolve on your machine only, against the tool's built-ins, never from a project directory.
So a cloned repository cannot point your decisions at a classifier of its choosing. The endpoint is built into
the adapter, whose id keys the cache and the lock, so no file and no variable moves it. The
clefandclef_flashadapters put one thing from a variable into their address, your account's id at Cloudflare. It must be 32 hex digits and nothing else, so it picks an account at that one service and never another address. A proxy named in your environment carries the connection there and sees ciphertext: the CLI trusts Mozilla's roots, never your platform's, and the SDK trusts what your Node trusts. - The SDK never searches.
loadtakes an explicit root, never fetches, and never writes. It hands each tenant their own vocabulary. So a server cannot be hijacked by whatever project a working directory holds. - A plan file holds the sentence and the classifier's numbers, in clear, and no setting's value, variable or key. It can choose among your installed reflexes and your words, never mint a value, and it runs only after your yes over the whole plan and each step's own confirm; a file whose pins moved is refused before anything runs.
- A playbook reaches only what you installed. Its steps are sentences, decided over your reflexes as typed ones are, under the same gate; it fills no value of its own, and it runs only after your yes over the whole plan, a destructive step confirming again at its turn.
Check it yourself
Nothing on this page needs a key to test. evoke run <call> runs a reflex by name and calls no classifier. The
replay adapter answers every decision from a file you write: set adapter = "replay" in evoke.toml and name
the file in EVOKE_ANSWERS. Testing shows the file. An answer that
tries to mint a value, a key no argument offers or a number that is not a probability, ends the decision with a
fault, and nothing runs. The repository's spec/ holds the transcripts and vectors behind the claims above, and
mise run test runs them without a key. The contained transcript shows a body refused past its declaration,
and a declared path the machine lacks refused before the body runs.
Reporting
Report a vulnerability privately, through GitHub's report form, never in a public issue.