Policy¶
Policy decides what may execute. It is evaluated before every operation, before authentication, and it is never optional and never skippable.
Location: wherever you keep it — point $RUNTIME_POLICY_FILE at it.
Ownership: yours. Runtime never creates it, and never overwrites it.
Policy has no environment-variable overrides. An env var must never be able
to widen governance — point RUNTIME_POLICY_FILE at a different file instead.
Schema version¶
Policy written by this release is schema v2. Absence still means legacy v1,
unknown keys fail with the key and line, and the parse window is independent of
the executable floor. A supported v1 document is migrated in memory without
changing its bytes. The running binary publishes both the migration window in
<Runtime Home>/specs/schema-contract.json and the strict v2 shape in
<Runtime Home>/specs/policy-schema.json.
Dry-run by default. --write is the guarded rewrite; it refuses a path inside the Runtime Home. The published window is <Runtime Home>/specs/schema-contract.json. See Upgrading.
Three layers, and only one of them is yours¶
| Layer | What it is | Can you change it? |
|---|---|---|
| Compiled safety profile | Invariants built into the binary: an operation no provider claims, a path that skips the audit write, a context-aware operation with no resolved target | No. Not by any file, flag or Control Plane value |
| Enterprise policy | A signed generation your Control Plane delivered | Only from the Control Plane. Runtime verifies it on every read |
| Your declarative policy | The document below | Yes — this page is about it |
The profile is a floor, not a stricter default. If it denies, the operation
is denied, and nothing above it can vote otherwise. runtime config validate
prints which version of it is in force:
Enterprise policy outranks your document rather than merging with it, and invalid enterprise state stops execution rather than falling back to yours — otherwise corrupting the bundle would be a way onto more permissive rules.
There is no off switch¶
A top-level enabled: false used to disable provider, binary and subcommand
governance in one line. Policy schema v2 removes that key entirely, because a
floor a file can remove is not a floor.
A legacy v1 document with enabled absent or true migrates safely in memory;
Runtime does not rewrite it. A legacy enabled: false document gets a blocking
message because removing it changes the document's stated meaning and needs an
operator decision. Remove the key deliberately (or delete the document), then
validate again. A v2 document containing top-level enabled is invalid.
Three governance surfaces¶
| Surface | Governs | Block |
|---|---|---|
| Provider Governance | runtime <provider> <operation> |
providers: |
| Binary Governance | runtime command run <binary> |
allowed_binaries: + command_policy: |
| File Governance | runtime files <operation> <path> |
file_policy: |
When a provider chooses the CLI transport for an operation, both of the
first two apply — the provider rule first, then allowed_binaries and
command_policy on top.
File Governance¶
New in 0.6.0, and it is a breaking change. File access is default-deny: a
policy document with no file_policy block permits no read, list, write,
append or delete at all.
file_policy:
read_roots:
- "." # the working directory, resolved per request
write_roots:
- "."
# Optional. These may only narrow the compiled budgets, never raise them.
max_read_bytes: 1048576
max_list_entries: 1000
max_depth: 32
max_edit_files: 1000
max_edit_batch_bytes: 33554432
max_edit_matches: 10000
max_edit_diff_bytes: 1048576
max_edit_seconds: 30
Read and write are separate authorities: read_roots permits read and
list, write_roots permits write, append and delete, and neither
implies the other. A path must resolve inside a granted root, so .. and an
absolute path elsewhere reach nothing.
A root that does not exist on a given machine is skipped rather than failing the document — one policy can name the trees several installs use.
Choosing roots¶
"." is resolved per request, not once
The sample above uses "." because it works anywhere. It is resolved
against the working directory of each command, so the grant moves with
wherever you happen to be standing: the same files read succeeds in one
directory and is refused in another. Convenient for a first run; a source of
confusing denials once capabilities run from CI, a scheduler, or an agent.
Write absolute paths in a policy you intend to keep.
Grant read widely, write narrowly. They are separate authorities precisely so they can differ, and the consequences are not symmetric — a wrong read exposes a file, a wrong write destroys one.
file_policy:
# Reading across the working tree is cheap.
read_roots:
- "/Users/you/projects"
# Writing is not. Name the directories capabilities may change.
write_roots:
- "/Users/you/projects/scratch"
- "/Users/you/projects/reports"
Do not grant write to a home directory or a tree of git checkouts. A root
covers everything beneath it, so /Users/you includes ~/.ssh, ~/.aws, and
the shell profile holding your tokens; a repository root lets a capability
rewrite tracked source with nobody reviewing the change. Runtime refuses its own
governance documents and the native credential stores, but it cannot know which
of your files matter.
If a capability needs to write into a repository, name that repository — not the tree containing it.
Budgets¶
The three read/list fields and the five max_edit_* fields bound what a single
operation may discover, read, transform and return. They may only be
narrowed by a document — a larger value is ignored, because a limit a
document can raise is a default rather than a limit.
Read/list result limits can return a stated truncation. Edit limits instead
refuse the batch before writing, because a partially selected estate is not the
operation the caller requested. Exact defaults, output behavior and the
redaction pass applied on the way out are on the
files provider page.
Provider Governance¶
How rules match¶
Operations are discovered from the Provider that owns them, not from a list here. The consequences:
- A provider with no entry is fully allowed.
runtime github repo listworks without a policy edit, and a new operation in a later runtime version never requires one. deniedprefixes match against"<operation> <args...>". A rule can deny a whole operation (delete) or one narrow case (workflow run deploy-prod.yml,api DELETE).allowed, when non-empty, restricts the provider to those prefixes.enabled: falsedisables the provider entirely.- Rules are evaluated on the operation, never the transport. A denial cannot be sidestepped by a provider moving an operation from REST to the CLI.
Governance is evaluated on what will actually execute
If a provider normalizes an argument, policy matches the canonical
form. This closed a real bypass: the github provider upper-cases an api
method, so api delete once slipped past the api DELETE denial and sent
a genuine DELETE. Any normalization performed after the policy check is a
bypass by definition.
A rule must name an operation that exists¶
The compiled default policy denies api DELETE rather than repo delete — because
there is no repo delete operation. That rule would read like governance
while enforcing nothing, and the destructive path is actually the api escape
hatch.
The report ends with policy warnings for any rule that can never match: a
providers.<name> rule naming an operation the provider doesn't expose, a
block for an unregistered provider, or a command_policy.rules.<binary> entry
for a binary absent from allowed_binaries.
A denial that matches nothing is worse than no denial — it produces false confidence. Check the warnings after every policy edit.
Locking a provider down to an allow-list¶
Anything not prefixed by one of those is refused.
Allow-list → deny-list was a deliberate change
Earlier versions used a global allowed_commands list, which no longer
exists. Installations that need an explicit allow-list must set
providers.<name>.allowed. See Migration Guides.
Binary Governance¶
allowed_binaries:
- gh
- git
- terraform
- gcloud
- kubectl
- oc
- aws
- az
- vault
- helm
- flux
- gsutil
- bq
- docker
- podman
- kustomize
- argocd
- istioctl
- pulumi
- packer
- sops
Only these binaries may execute at all through runtime command run. A
binary absent from this list is refused, whatever the subcommand rules say.
Subcommand rules¶
admitted_helpers — letting a tool reach its credential helper¶
Some tools delegate authentication to a separate executable. docker finds its
registry credentials by launching docker-credential-gcloud; git can be
configured the same way.
Runtime resolves and pins the binary you asked for, then runs it in a bounded
environment with a fixed minimal PATH — the caller's PATH is deliberately
not forwarded, because that is what a substitution attack edits. A helper living
in /opt/homebrew/bin is therefore unreachable, and docker push fails inside
Runtime while the identical push succeeds outside it.
admitted_helpers opens exactly that door and no wider:
Four properties, each deliberate:
- Absolute paths only. A relative path is resolved against something Runtime does not control. A non-absolute entry is rejected when the policy loads, not when a push fails later.
- Scoped to one parent binary. A helper admitted for
dockeris not reachable byterraform. The admission is a pair, never a global allowance. - Only the named files are exposed. Runtime stages each admitted helper into
a directory it controls and puts that on
PATH. The helper's own directory is never added — admitting one binary in/opt/homebrew/binmust not admit the several hundred beside it. - Empty by default. With no
admitted_helpers, nothing is admitted and the push still fails. Widening this is always a deliberate act.
Requires schema_version: 3 (Runtime 0.9.1 or later).
command_policy:
# Refused outright even if they were ever added to allowed_binaries
denied_binaries:
- rm
- sudo
- chmod
- curl
- wget
- ssh
rules:
git:
denied:
- push --force
- push -f
- reset --hard
- clean -fd
- filter-branch
- update-ref -d
terraform:
denied:
- destroy
kubectl:
denied:
- delete namespace
helm:
denied:
- uninstall
Matching semantics, which differ between the two lists:
| List | Matching |
|---|---|
denied |
A contiguous run of argument tokens anywhere in the command |
allowed |
Prefix match |
Denials always win.
Because denied matches anywhere, a leading global flag cannot bypass a rule:
git -C /some/dir push --force is caught exactly like a bare
git push --force.
Subcommand rules are evaluated before Runtime Context injection, so an
injected flag (--project=, -n <namespace>) can never be used to slip past a
denial.
What the compiled default rules protect¶
The shipped defaults target one thing consistently: irreversible actions.
| Binary | Denied | Why |
|---|---|---|
git |
force-push, hard reset, clean, filter-branch, ref deletion | Rewrites or discards history |
terraform, pulumi |
destroy |
Tears down real infrastructure |
helm, istioctl |
uninstall |
Removes a release or the mesh control plane |
flux |
delete, uninstall |
Removes GitOps-managed resources or Flux itself |
kubectl / oc |
delete namespace / delete project |
Bulk resource destruction |
gcloud |
project and service-account deletion | Identity and account loss |
aws |
s3 rb --force, ec2 terminate-instances, iam delete-user, rds delete-db-instance |
Bulk data and identity deletion |
az |
vm delete, group delete, ad user delete, keyvault delete |
Same profile as aws |
vault |
kv delete, kv metadata delete, secrets disable |
Permanent secret removal |
gsutil |
rm -r, rb |
Recursive object and bucket deletion |
bq |
rm -f, rm -r -f, rm -f -r |
-f skips the confirmation prompt |
docker, podman |
system prune -a, volume rm, run --privileged |
Bulk pruning; privileged run is a container-escape risk |
argocd |
app delete, cluster rm, repo rm |
Detaches or deletes GitOps state |
kustomize, packer and sops carry no denial rules — none of them has a
subcommand that destroys existing infrastructure. kustomize only renders YAML
to stdout, packer only builds new images, and sops encrypts and decrypts
files in place.
Two denials that are not in your document¶
The compiled floor refuses two families of command regardless of what any policy — local or enterprise — says about them, because a denial a document can remove is a suggestion.
Credential export (secret-export-operation): the operation's output is
a secret. gcloud auth print-access-token, kubectl get secret, vault kv
get, aws secretsmanager get-secret-value, sops -d, git credential fill.
A tool using a credential is untouched — that is the whole native-credential
model.
Tool reconfiguration (tool-lifecycle-mutation): the operation changes the
tool rather than the system you are operating on. gcloud config set, gcloud
components install, kubectl config use-context, oc login, git config
--global, helm plugin install, docker login, gh extension install.
Reading the same configuration is fine (kubectl config view, gcloud config
get-value project, git config --get).
The reason these are a floor rather than a rule: Runtime decides whether an operation is allowed by pinning the exact binary and reading where your tool is pointed. A command that installs a component into that install root or switches the current context changes what every later governed operation does. Do those in your own shell, deliberately.
See command run for the full
lists and the exact refusal text.
Denials are audited¶
A denial is recorded exactly like a success or a failure. That is the point: the audit log answers "was this attempted and refused", not just "what happened".
The runtime tells you when it is the one refusing:
Common tasks¶
After any edit:
New policy defaults never reach an existing install
An env-selected community policy is yours, so a release adding allowed_binaries
entries changes nothing for a machine that already selected a file.
runtime config validate summarises the difference;
runtime config diff policy lists every binary. See
the upgrade trap.
Operational examples on this site were verified against Runtime 0.9.8. After bootstrap, the version-exact files in Runtime Home win.