Permissions
Control which actions require approval to run.
hordecode uses the permission config to decide whether a given action should run automatically, prompt you, or be blocked.
As of v1.1.1, the legacy tools boolean config is deprecated and has been merged into permission. The old tools config is still supported for backwards compatibility.
Actions
Each permission rule resolves to one of:
"allow"— run without approval"ask"— prompt for approval"deny"— block the action
Auto mode
Start hordecode with --auto to automatically approve permission requests that are not explicitly denied.
horde --autoYou can also use auto mode with horde run.
horde run --auto "Refactor this module"Explicit "deny" rules are still enforced. Auto mode only changes requests that would otherwise ask for approval.
In the TUI, open the command palette and select Enable auto-approve permissions or Disable auto-approve permissions to change modes. When auto mode is active, the prompt displays a muted auto indicator next to the current agent.
Auto mode covers the whole running process, including requests raised by subagents, and requests that were already waiting are released when you switch it on. Requests that always ask, such as a shell command that deletes files, still prompt in auto mode; when nobody answers within 60 seconds the request is rejected with a message that tells the model to skip that step and continue. Set HORDECODE_SKIP_ALL_FORCED_ASK_TIMEOUT_MS to change the wait.
Configuration
You can set permissions globally (with *), and override specific tools.
{ "$schema": "https://hordecode.com/config.json", "permission": { "*": "ask", "bash": "allow", "edit": "deny" }}You can also set all permissions at once:
{ "$schema": "https://hordecode.com/config.json", "permission": "allow"}Granular Rules (Object Syntax)
For most permissions, you can use an object to apply different actions based on the tool input.
{ "$schema": "https://hordecode.com/config.json", "permission": { "bash": { "*": "ask", "git *": "allow", "npm *": "allow", "rm *": "deny", "grep *": "allow" }, "edit": { "*": "deny", "packages/web/src/content/docs/*.mdx": "allow" } }}Rules are evaluated by pattern match, with the last matching rule winning. A common pattern is to put the catch-all "*" rule first, and more specific rules after it.
Wildcards
Permission patterns use simple wildcard matching:
*matches zero or more of any character?matches exactly one character- All other characters match literally
Home Directory Expansion
You can use ~ or $HOME at the start of a pattern to reference your home directory. This is particularly useful for external_directory rules.
~/projects/*->/Users/username/projects/*$HOME/projects/*->/Users/username/projects/*~->/Users/username
External Directories
Use external_directory to allow tool calls that touch paths outside the working directory where hordecode was started. This applies to any tool that takes a path as input (for example read, edit, glob, grep, and many bash commands).
Home expansion (like ~/...) only affects how a pattern is written. It does not make an external path part of the current workspace, so paths outside the working directory must still be allowed via external_directory.
For example, this allows access to everything under ~/projects/personal/:
{ "$schema": "https://hordecode.com/config.json", "permission": { "external_directory": { "~/projects/personal/**": "allow" } }}Any directory allowed here inherits the same defaults as the current workspace. Since read defaults to allow, reads are also allowed for entries under external_directory unless overridden. Add explicit rules when a tool should be restricted in these paths, such as blocking edits while keeping reads:
{ "$schema": "https://hordecode.com/config.json", "permission": { "external_directory": { "~/projects/personal/**": "allow" }, "edit": { "~/projects/personal/**": "deny" } }}Keep the list focused on trusted paths, and layer extra allow or deny rules as needed for other tools (for example bash).
Available Permissions
hordecode permissions are keyed by tool name, plus a couple of safety guards:
read— reading a file (matches the file path)edit— all file modifications (coversedit,write,apply_patch,notebook_edit; a deny rule also hidesmultieditandhashline_edit)glob— file globbing (matches the glob pattern)grep— content search (matches the regex pattern)bash— running shell commands (matches parsed commands likegit status --porcelain)task— launching subagents (matches the subagent type)skill— loading a skill (matches the skill name)lsp— running LSP queries (currently non-granular)question— asking the user questions during executionwebfetch— fetching a URL (matches the URL)websearch— web search (matches the query)external_directory— triggered when a tool touches paths outside the project working directorydoom_loop— triggered when the same tool call repeats 3 times with identical input
Defaults
If you don’t specify anything, hordecode starts from permissive defaults:
- Most permissions default to
"allow". doom_loopandexternal_directorydefault to"ask".readis"allow", but reading.envfiles asks first (.env.examplefiles are allowed):
{ "permission": { "read": { "*": "allow", "*.env": "ask", "*.env.*": "ask", "*.env.example": "allow" } }}What “Ask” Does
When hordecode prompts for approval, the UI offers three outcomes:
once— approve just this requestalways— approve future requests matching the suggested patterns (for the rest of the current hordecode session)reject— deny the request
The set of patterns that always would approve is provided by the tool (for example, bash approvals typically whitelist a safe command prefix like git status*).
Guardrail profile
ctrl+p, then Agent, Guardrail profile switches between careful, normal and auto.
carefulasks before edits, shell commands and web fetches even when your rules allow them. A pattern the session has already approved is not asked again, and deny rules still win.- The choice is remembered per project and restored on start.
- Subagents inherit careful mode and only the approvals their parent gave.
Audit log
With HORDECODE_PERMISSION_AUDIT=1 (or the permission_audit switch), every permission decision and reply is written as one JSON line to <data>/log/permission.jsonl. The file rotates at 5 MiB (HORDECODE_PERMISSION_AUDIT_MAX_BYTES) and keeps three older files.
Each line carries prev, the SHA-256 of the line before it, and the chain continues across rotation. Every line also carries the time it was written. A decision line carries kind, requestID, sessionID, permission, the patterns it matched, the tool call ID and the outcome: allow, deny, deny_non_interactive, reject, reject_with_feedback, timeout, interrupted or defect. A reply line carries kind, requestID and the reply: once, always or reject. Credentials in URLs, bearer tokens and secret-like key=value pairs are redacted before the line is written.
The head record
The newest line and the oldest retained file are anchored by a head record, permission.jsonl.head, written next to the log. It holds the SHA-256 of the newest line and the SHA-256 of the first line of the oldest retained file. It is written through a temporary file, so a crash never leaves a half-written head.
An audit log written before 0.1.5 has no head record yet. While its lines are chained, horde debug audit-verify reports head record missing for it, because a truncated tail or a removed oldest file cannot be checked without the head. The next logged decision writes the head, and the log verifies from then on. A log whose lines all predate the hash chain is counted as legacy and verifies on its own.
Verifying the log
horde debug audit-verify [file] walks the rotated files oldest first. It prints ok files=<n> lines=<n> legacy=<n> when the log verifies, and broken files=<n> lines=<n> legacy=<n> at <file>:<line>: <reason> with exit code 1 when it does not. With no log file it prints no audit log found: <file>.
The reasons it reports:
| Reason | What it means |
|---|---|
prev mismatch: expected <hash>, found <hash> | The line no longer links to the line before it, so it was edited or replaced. |
invalid JSON | The line is not valid JSON. |
line is not an object | The line is valid JSON, but not a JSON object. |
legacy line after a chained line | A line without prev follows a chained line. |
zero prev after earlier lines | A chained line starts with a zero prev although earlier lines exist. |
head record missing: truncation of the tail or of the oldest file cannot be checked | The log has chained lines but no head record. |
head record unreadable | The head record exists and does not parse. |
tail mismatch: head expects last line <hash>, found <hash> | The newest line is not the one the head record names, so lines were removed from the end of the newest file. |
oldest file mismatch: expected first line <hash>, found <hash> | The first line of the oldest retained file is not the one the head record names, so that file was replaced or removed. |
Lines written before the chain existed are counted as legacy. The first chained line is the anchor of the chain, because its predecessor may already have been rotated out.
Probes
A shell command that deletes files runs a deny-only probe first, for the bash permission and for external_directory. A probe that a deny rule answers denies the command and is written to the log as a decision with the outcome deny. A probe that no deny rule answers decides nothing, and it is not written.
Scope of the chain
The chain is a local integrity check without a secret key, which sets its scope:
- Someone who can write the file can also rewrite the whole chain consistently, and the head record lives next to the log rather than apart from it. Keep the log where only you can write, or copy it elsewhere if you need tamper evidence against other users.
- When the oldest file rotates out, the first remaining line becomes the start of the chain and the head record is rewritten to name it.
- Two hordecode processes writing to the same log at the same time can interleave lines and break the chain; verify reports the first such line.
Agents
You can override permissions per agent. Agent permissions are merged with the global config, and agent rules take precedence. Learn more about agent permissions.
{ "$schema": "https://hordecode.com/config.json", "permission": { "bash": { "*": "ask", "git *": "allow", "git commit *": "deny", "git push *": "deny", "grep *": "allow" } }, "agent": { "build": { "permission": { "bash": { "*": "ask", "git *": "allow", "git commit *": "ask", "git push *": "deny", "grep *": "allow" } } } }}You can also configure agent permissions in Markdown:
---description: Code review without editsmode: subagentpermission: edit: deny bash: ask webfetch: deny---
Only analyze code and suggest changes.