Skip to content

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.

Terminal window
horde --auto

You can also use auto mode with horde run.

Terminal window
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.

hordecode.json
{
"$schema": "https://hordecode.com/config.json",
"permission": {
"*": "ask",
"bash": "allow",
"edit": "deny"
}
}

You can also set all permissions at once:

hordecode.json
{
"$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.

hordecode.json
{
"$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/:

hordecode.json
{
"$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:

hordecode.json
{
"$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 (covers edit, write, apply_patch, notebook_edit; a deny rule also hides multiedit and hashline_edit)
  • glob — file globbing (matches the glob pattern)
  • grep — content search (matches the regex pattern)
  • bash — running shell commands (matches parsed commands like git 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 execution
  • webfetch — fetching a URL (matches the URL)
  • websearch — web search (matches the query)
  • external_directory — triggered when a tool touches paths outside the project working directory
  • doom_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_loop and external_directory default to "ask".
  • read is "allow", but reading .env files asks first (.env.example files are allowed):
hordecode.json
{
"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 request
  • always — 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.

  • careful asks 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:

ReasonWhat 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 JSONThe line is not valid JSON.
line is not an objectThe line is valid JSON, but not a JSON object.
legacy line after a chained lineA line without prev follows a chained line.
zero prev after earlier linesA 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 checkedThe log has chained lines but no head record.
head record unreadableThe 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.

hordecode.json
{
"$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:

~/.config/hordecode/agents/review.md
---
description: Code review without edits
mode: subagent
permission:
edit: deny
bash: ask
webfetch: deny
---
Only analyze code and suggest changes.