Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Permissions

[permissions] says which tool calls run without asking and which never run. It can only narrow what a tool's level allows: nothing here makes a tool quieter than its level (Permissions explains the levels).

[permissions]
# Level 2 calls that run without a prompt.
approve = [
    "exec(cargo) args(*)",
    "exec(git) args(diff *)",
]

# Never allowed, at any level. The model receives matching patterns and reasons.
deny = [
    "exec(git) args(push *)",
    "exec(rm) args(*)",
    { pattern = "edit(Cargo.lock)", reason = "Use the package manager to update the lock file." },
]

# Written by heph when you press A on a prompt. Matches like `approve`.
promoted = [
    "exec(cargo) args(clippy *)",
]
KeyMeaning
approvePatterns whose matching Level 2 calls run unasked.
denyPatterns whose matching calls are refused, at any level, before anything else is considered.
promotedPatterns heph appends when you answer A to a prompt. They match exactly like approve; the separate list keeps what you wrote apart from what heph wrote.

Project lists are added to the user's, never replace them. A team's deny list in .heph/config.toml binds every member on top of their own.

web_fetch is the one tool whose approvals are not patterns: a fetch is allowed by its host, in [network]. A web_fetch(...) pattern here still matches the URL as text, which is what deny = ["web_fetch"] is for; to allow a host, add it to [network] allow.

Authored deny reasons

A deny entry is a pattern string or a table with a required string pattern and an optional string reason. Both forms may coexist; a table without a reason is equivalent to the string. Unknown fields, missing or invalid patterns, and nonstring fields are config errors. approve and promoted still accept only strings, as does [network].deny for hosts.

[permissions]
deny = [
    "exec(rm) args(*)",
    { pattern = "edit(generated/**)", reason = "Edit the source schema and regenerate instead." },
    { pattern = "edit(generated/api/**)", reason = "API bindings come from schemas/api.yaml." },
]

Editing generated/api/client.rs returns both matching patterns and their reasons to the model, and shows the same explanation in developer status. There is no permission prompt and nothing executes. Every matching entry is included in array order, user rules first and project rules second, even when patterns repeat. A project cannot replace a user's reason or remove a user denial.

Reasons are plain text, with no interpolation or shell expansion. Empty or whitespace-only reasons add no explanation; nonblank text and line breaks are preserved, with continuation lines indented under Reason:. They reach the selected model only when the rule denies a call, as ordinary tool-result feedback, not higher-priority instructions. Avoid secrets, since normal tool results may be retained. Suggested alternatives still require permission.

Typed serialization emits reasonless rules as strings and reason-bearing rules as tables, without promising exact lexical formatting. Approval promotion leaves untouched deny entries, reasons, and comments intact.

Pattern syntax

A pattern is tool(glob), a bare tool, or exec(program) args(glob).

  • tool is a tool name from the catalog: lowercase letters, digits, underscores.

  • glob matches the call's principal parameter: the command for shell, the URL for web_fetch, the path for view, ls, glob, grep, stat, edit, create, search_replace, and blame. Tools with no principal (status, branches, ask_user) match only as a bare name.

  • A bare tool matches every call of the tool.

  • * matches any run of characters. For the path tools it stops at /; ** crosses directories. For URLs * matches anything, slashes included.

  • ? matches one character.

No wildcards in a shell command

A shell (and background) pattern is an exact command, with no * or ?. A shell call is a string run through sh -c, so a wildcard in the pattern would cover the shell metacharacters that chain a second command: shell(cargo *) would match cargo x; rm -rf ~, approving a command it never meant to. A pattern with a wildcard for shell or background is a configuration error.

exec pins the program and globs the arguments

To approve a program with a wildcard, use exec, which runs one named program directly -- never through a shell -- with its arguments passed to it literally. The pattern is exec(program) args(glob): the program is exact (no wildcard), the args glob matches the arguments joined by spaces. exec(cargo) args(*) approves any cargo invocation and only cargo; nothing an argument contains can start a second process, because there is no shell to interpret it. exec(program) with no args(...) matches only a call with no arguments.

Examples:

PatternMatches
exec(cargo) args(*)Any cargo invocation, any arguments, run directly
exec(cargo) args(test *)cargo whose arguments begin test
exec(make)make with no arguments
shell(make release)Exactly the command make release, run through a shell
web_fetchEvery fetch
edit(src/*)Files directly in src/, not in subdirectories
edit(src/**)Anything under src/
edit(**/*.lock)Any lock file at any depth
view(.env)That one file (in deny, keeps secrets out of the context)

An exec call may also set child-only environment variables with its env map. The prompt displays those overrides, but patterns match only the program and arguments. For example, exec(cargo) args(*) covers cargo test both with and without env: {"RUST_BACKTRACE": "1"}; no approval of the generic env launcher is needed.

What the prompt offers

When a prompt opens, heph proposes a pattern to remember: for an exec call, the program and args(*); for a path, its directory and *; for a shell command, the command exactly, since no wildcard is safe there. A web_fetch prompt proposes the URL's host instead (Network). e on the prompt edits it before a (session) or A (written here). The = line under the pattern spells out its meaning in plain words, generated from the pattern itself:

PatternExpansion
exec(cargo) args(*)any exec of cargo, any arguments; only cargo is run, and each argument is passed to it literally, never through a shell
shell(make release)exactly the command "make release", and nothing else; a shell approval is one exact command, with no wildcards
shellevery call of shell
edit(src/*)edit on any file directly under src/, not in subdirectories

Repeated-call checkpoints

[permissions.repeat]
soft_limit = 3
hard_limit = 12

soft_limit controls assessment of suspected recent repetition. hard_limit requires a human when unchanged outcomes or cycles persist. The defaults are 3 and 12, and 0 < soft_limit < hard_limit is required. These are not lifetime call counts: productive edit/test iterations do not exhaust an allowance.

Each turn retains 32 completed attempts, including denials. Detection considers unchanged results, matching calls dominating at least half of the last eight activities, and recurring cycles of two to six operations. Content states and outcomes distinguish productive work from oscillation. A progress verdict clears soft suspicion; a new evidence pattern or a sufficiently long gap clears the old episode. Human approval clears the turn's current pattern history and allowances, not ordinary permissions.

Commands allow three completed attempts before the earliest assessment, so transient compilation failures have a small retry allowance. A job still running may be polled eight times unchanged before assessment; polling raises the effective soft and hard limits to at least 8 and 32. It is not permanently exempt.

A project [permissions.repeat] table replaces the user's pair as a unit; omitted keys in that table take their built-in defaults. Without a project table, the user's pair is retained. These limits refresh with live permission policy, just like the pattern lists.

The progress-check role uses the selected profile's fast tier by default, respecting tier overrides. Older custom layouts without this role use their quick tier. With neither route, the checkpoint requires human approval. See The repeated-call anomaly for the prompt and renewal behavior.

Order of evaluation

For each call, in this order, the first that applies decides:

  1. Any deny match refuses the call, with every matching rule's explanation. Otherwise, a [network] deny host refuses web_fetch.

  2. The hard repeated-call limit requires a human. At the soft limit, a fast model assesses progress; looping, uncertainty, or failure requires a human. A positive assessment clears only this repetition checkpoint, not policy.

  3. Level 0 and 1 calls run.

  4. An approve or promoted match runs a Level 2 call.

  5. A pattern approved with a this session runs it.

  6. For web_fetch, a host on the allowlist runs it at Level 1, and a host approved with a this session runs it.

  7. --allow-tools, --yes, or --yes-really runs it (--yes-really alone covers Level 3).

  8. Otherwise, the prompt, or a denial when no one is there.

Flags

The same patterns work on the command line, for one run:

heph exec --allow-tools 'exec(cargo) args(*)' 'fix the tests'