Approval modes and permissions
Approval is layered: a mode for the chat, permission rules that refine it, and guardrails that projects and administrators can add.
The four modes
From most to least restrictive:
| Mode | Edits | Shell commands |
|---|---|---|
| Plan mode | blocked | blocked (read-only research) |
| Ask every time | ask | ask |
| Auto-approve edits | automatic | ask |
| Full auto | automatic | automatic (sandboxed), with the guardrails below |
Plan mode
The agent researches and proposes a plan; writes and shell commands are blocked. You accept the plan, request changes, or reject it.
- Checks it can run. The plan lists the checks it will run, and under them Houston shows whether they can run on this machine: whether the runtimes and dependencies are installed, and whether shell commands can reach the network to install them (for example, "choose Allow for run on the install command").
- Houston works this out from the machine without running anything, and tells the agent about any problem when you accept, so it can say a check couldn't run instead of claiming it did.
- Switching is immediate. Turning Plan mode on or off takes effect at once, even mid-turn, and Houston tells the agent before its next step.
Guardrails in full auto
The sandbox can read your whole filesystem, so a few controls hold even in full auto:
- Network, per destination. Approving a fetch to one host doesn't open egress to another. See "Network calls in full auto" below.
- Shell network stays on the allowlist. Shell commands reach package registries and code hosts (plus any domains you add) through the egress allowlist. When one tries a host that isn't on it, the connection is refused and Houston asks whether to allow that host for this chat. When you allow it, the agent retries the command if that's safe. Blanket outbound access is never automatic: with egress set to "All domains", shell commands start offline and Houston asks once, the first time one fails for lack of network (the shell-network consent).
- Paths outside the project. A shell command that names one, quoted or not, asks first.
/dev/nulland the temp directory the sandbox can already write to don't count. - Reading a credential file (a
.env, a private key,.aws/credentials, and the like) asks, even though reads otherwise never do. "Allow for run" on that prompt stops it asking about credential reads for the rest of the conversation, and anallowpermission rule opts a path in permanently. - Changing a credential file asks too: a file edit that writes one, or a shell command that redirects into, copies over, edits in place, or removes one (
> .env,cp .env.example .env,sed -i … .env,rm .env). Templates such as.env.exampledon't count. - Git hooks and settings that run programs. See "Git hooks and settings" below.
Git hooks and settings
Shell commands can write anywhere in the project, .git included, so everyday git keeps working in full auto: commits, git config, git push -u, hook installers, submodules. A few things in .git are programs that git runs by itself, though: hooks, and settings such as core.fsmonitor or a filter driver. Your own terminal's git (or a shell prompt that runs git status) would run them outside the sandbox, and neither git status nor a diff shows them.
So after each shell command Houston checks for those and nothing else:
- Nothing to see for ordinary git. Your name and email, remotes, and upstream branches never count, and a command that doesn't touch
.gitcosts a few file checks. - Husky's setup is noted, not asked about. Its hooks folder (
.husky) is checked in, so you can read what it runs. - Anything else pauses once and lists what changed: a new or changed hook, a setting that names a program, a nested repository registered as a submodule that carries one, or a file that points git at another folder's settings. Undo puts back only those items (other changes the same command made stay), and Keep leaves them, for example a hook tool you meant to install. If you keep something, the reply says how to remove it by hand later.
- Headless runs and runs nobody is watching undo the change and say so, since nobody can approve it.
- File edits to
.git/hooks/…,.git/config, or similar ask first, like credential files do.
The check runs after the command, so a program that a shell prompt ran in the moment between the command finishing and the question appearing has already run. A background command is checked when its output is read or it is stopped.
Network calls in full auto
GitHub reads run without asking: gh_pr_list, gh_pr_view, gh_pr_checks, gh_issue_list, gh_issue_view, gh_run_list, gh_run_view.
Everything else asks once per destination, and "Allow for run" covers only that destination (the prompt says which):
web_fetchasks once per site, andweb_searchonce per chat. Settings → Permissions → Web reads in full auto can let these run without asking. Either way, a URL or query carrying a credential is refused.view_localhostasks once per host, since a page can load from the internet. A call that clicks or types (steps) asks once more per host.- GitHub writes (open a PR or issue, comment, review, reopen, mark ready or draft) share one grant per repository, so allowing a comment on one repository never allows anything on another. The repository is the one
ghwould pick (yourgh repo set-defaultchoice included), and every call is told to use exactly that one, so pointing the remotes or the default repository elsewhere asks again. - Merging a PR, closing a PR or issue, checking out a PR, and creating a repository each ask on their own: allowing a comment never allows a merge.
Throwing away work always asks
These ask in every mode, full auto included. The sandbox keeps commands inside the project, but the project is exactly what these destroy, and undo doesn't cover shell commands.
git reset --hard, a forcedgit clean,git checkout -- ., orgit restoreof your changesrm -rof anything but build output and dependencies (node_modules,dist,build,target,__pycache__, and the like are fine)find … -delete- deleting a branch or a stash, rewriting history
- a forced or deleting
git push
Quoting a word, splitting it across lines, or running it through bash -c doesn't hide one. "Allow for run" doesn't stop these asking; an allow permission rule for that exact command does (the "Always allow" choice on the prompt saves one).
Which mode a new install starts in
- Full auto where the sandbox confines shell commands: macOS, and Linux with bubblewrap.
- Ask every time where it can't (Windows, or Linux without bubblewrap), since every command would run unconfined.
An existing install keeps the mode it last used. The desktop app and the terminal start in the same saved mode. --approval or --full-auto overrides it for one terminal session, and a headless run starts in plan unless told otherwise.
Approving a call
On any single tool call you can:
- approve once
- deny
- allow for the run: auto-approve that kind of tool for the rest of the conversation (for a network call, just that one destination, which the prompt names)
- always allow / always deny: save a permission rule so the choice persists
Reading the diff. A file edit shows a real diff before you approve it: the changed part with a few lines of context (long untouched stretches are folded away with a note saying how many), line numbers in the gutter, the changed words picked out inside a modified line, and syntax colouring.
Denying with a reason. A refusal can carry guidance, and the agent is told what it says, so "no, and here's what to do instead" lands in the same step.
- Desktop app: type it in the box under the approval buttons.
Enterdenies with that reason. - Terminal: pick "Deny with a reason…", or just type the reason at the approval prompt. Anything that isn't a shortcut key is read as a denial plus that explanation.
Terminal approval keys: y allow, n deny, a allow for the run, ! always allow, x always deny.
Stopping a run while an approval is waiting is recorded as an interruption, not a refusal, so the agent isn't told you rejected something you never answered.
Switching mode
- Terminal:
Shift-Tabsteps through the modes, from most restrictive to least and back around to plan./approval <mode>sets one directly. - Works mid-turn. It applies to the run in progress: if a turn stops for an approval you'd rather not keep answering, loosen the mode and the same turn carries on under it.
- Where it's shown: next to the spinner while a turn runs, and on the status line above the composer the rest of the time.
- Per chat. The mode belongs to the chat, like its model (see "Each chat keeps its own settings"). New chats start in the mode chosen in Settings > Permissions > Approval mode.
Permission rules
Rules (in Settings) are finer-grained than the mode: allow, deny, or ask, matched on the tool plus a glob over its target. For example:
- allow
run_shellmatchinggit * - deny anything matching
*rm -rf* - always ask before
write_fileundersrc/secret/**
Order matters. Rules are checked before the mode, and the first match wins. Settings lists them numbered in the order they're checked; drag a rule or use its arrows to move it. By tool groups the list per tool instead.
Settings flags problems: a rule that can never apply because an earlier one already matches everything it would (a * pattern for the same tool, or the same pattern), an empty pattern (it matches nothing), and an allow of *.
History. Each rule notes when it was added (and whether from an approval prompt or by hand) and when it last decided a call, which helps you prune rules you no longer need.
Test a call takes a tool and a command, path, URL, or query, and shows what the rules decide and which rule decides it. It uses your unsaved edits and the administrator and project rules. It covers rules only, so a write outside the project or to a sensitive file can still ask.
Edit as text shows the rules as JSON, one per line, in the same shape as permissionRules in settings.json, for bulk edits or moving rules between machines. It checks the text as you type (a misspelt field is an error, not ignored), flags rules that could never apply, says what applying would add, remove, or reorder, and keeps each surviving rule's history.
Rules can't be dodged by respelling
Matching normalizes the target first:
- File rules are resolved and anchored to the workspace.
src/*covers the same file written as an absolute path, and a climb likesrc/../../etc/passwddoesn't slip past it. - URL rules are case-insensitive on scheme and host.
- Shell rules see through quoting, backslash escapes, and bash's
$'...'escapes, sodeny *rm -rf*still fires on"rm" -rf,r\m -rf, or$'\x72m' -rf. - Line continuations. A line ending in a backslash continues onto the next, as in bash, so a deny fires on
rmsplit across two lines, and an allow must cover the joined command.
Allow rules only see through quoting where it changes nothing. "git" status is git status, but a quoted or escaped *, ~, $, space, or > isn't the bare one, since the shell treats them differently:
- allowing
rm '*'(one file named*) never allowsrm * - allowing
rm -rf '~'(orrm -rf $'\x7e') never allowsrm -rf ~ - an empty quoted argument counts as one: allowing
'' rm -rf /(which runs nothing) never allowsrm -rf / - quoted or escaped whitespace is part of an argument, as in bash: allowing
rm 'my file'(two spaces) never allowsrm 'my file', and allowingfind . -delete \(find gets a lone space and stops) never allowsfind . -delete
Spacing outside quotes doesn't matter. A rule an earlier version of Houston saved with that whitespace collapsed allows what its text says. One that ends in a lone backslash matches nothing, and Clean up rules removes it.
Patches
An apply_patch call is checked against every file it adds, changes, deletes, or moves to:
- one denied file denies the whole patch
- one
askfile asks - an
allowcovers it only when every file is allowed
"Always allow" or "Always deny" on a patch saves one rule per file.
Exact rules and what "Always allow" saves
A rule marked Exact matches only its one target: * is an ordinary character, and a folder doesn't cover the files inside it.
| "Always allow" on | Saves |
|---|---|
| a file | an exact rule, so approving a write to a file named *, or to a folder path, allows that path and nothing else |
a web_fetch | the site (https://example.com, covering every page there) |
a web_search | every search |
| a shell command | a command prefix, such as npm install |
- A shell command it can't shorten to a prefix is saved whole, and exact when it holds
*, so approvingrm *never allowsrm -rf ~. - A
cdinto the project needs no rule (an allowednpm installalso coverscd src && npm install), so it's left out. Any other change of directory, such ascd -,popd,cd $DIR, or acdout of the project, still asks, and is saved whole and exact. - A shell rule like
rm *saved by an earlier version is still a pattern. Tick Exact on it to narrow it. - Always deny saves a regular pattern, which can only be broader than the call it blocked.
Project guardrails
A repo can ship a .houston/settings.json with its own deny / ask rules, checked before your global rules. They always apply and can only tighten, so cloning an untrusted repo can never auto-approve anything.
Trusted folders
The same project file can also define allow rules, hooks, and mcpServers. Those elevate (they auto-approve matching actions or run processes as you), so Houston ignores them until you explicitly trust the folder.
- How you're asked: the desktop app shows a banner above the composer, and the interactive terminal asks at session start (Trust / Not now / Never).
- Re-asked on change. Your decision is bound to a fingerprint of that elevating config. If it changes (say a pull adds a hook), the folder drops back to untrusted and Houston asks again.
- Headless runs never prompt; they note when an untrusted folder's extra config is being ignored.
- Once trusted: project
allowrules sit below your own rules (they fill gaps, never override you), project hooks run after yours, and project MCP servers appear namespaced asmcp__proj-<id>__<tool>.
Reviewing decisions. Settings > Permissions > Trusted folders lists every folder you answered for (trusted or never), with a Forget button. In the terminal, /trust shows the current folder's state, and /trust forget clears the decision and asks again on the spot. Forgetting is how you reverse a "Never" or retire a stale "Trust".
Managed policy
On a managed device, an administrator can ship a machine-wide managed-settings.json with deny / ask rules that outrank both project guardrails and every user's own rules. It too can only tighten, and it lives in a root-owned location the user can't edit.
How the tiers stack
Settings > Permissions > All sources lists every tier in the order it's checked:
- the administrator's policy
- the open project's guardrails
- your own rules
- the project's allow rules (with whether they're in effect)
- the chat's approval mode
The project's hooks and MCP servers show read-only on the Hooks and MCP Servers pages; edit them in .houston/settings.json.