Approval Modes
Updated October 1, 2026
When an agent needs to run a tool (editing a file, running a command, searching your codebase), it asks for permission. crystl intercepts that request and decides whether to auto-approve it or float it to you as a glass panel.
The setting is called auto-approval. It has three states.
The three modes
off
crystl auto-approves nothing. Every tool call waits for you. A floating approval card appears with allow and deny buttons.
Best for: unfamiliar code, sensitive operations, watching what an agent does step by step.
in gem
Reads anything. Makes changes only inside this gem. Runs git that cannot lose work. Asks before anything else.
That sentence is the whole contract. In detail:
- Reads auto-approve.
Read,Glob,Grep,WebSearch,WebFetch, and the subagent toolsAgentandExplore. Reads change nothing, so they never wait. - Writes auto-approve inside the tree.
Edit,Write, andNotebookEditgo through when the target path is inside the shard’s own worktree or repo root. A write outside that tree asks, and so do writes into.git/. Writes into.crystl/and into the agent’s own control files (.claude/settings*.json, itshooks/,agents/,commands/, andskills/directories,.agents/skills/, and.mcp.json) ask too, because those files change what runs or what is permitted without a panel. Codex edits arrive asapply_patchand follow the same rule: an in-tree add or update goes through, a deletion or an out-of-tree path asks. - Safe git auto-approves.
git add,commit,status,diff,log,show,rev-parse,git stash list/git stash show, andgit brancheither in its listing forms or creating a branch by name, which is additive. Anything that can discard work asks. That includes baregit stash,stash drop,stash clear, andgit branch -D. One plain invocation only. - Everything else asks. Any other command, any other tool. The one exception is a worker’s own reporting helpers (
quest_msg,quest_task,quest_summary,quest_handoff,quest_heartbeat,quest_claim,quest_unblock,quest_level), which are installed only into quest and fanout shards. Making a worker ask permission to report its own status is the opposite of fanning out and walking away. They ask under off like everything else.
Writes into .git/ still ask
.git/ sits inside the tree and is not workspace. A write to .git/config or .git/hooks/* installs something a later git run executes, often outside the run that wrote it, so it raises a panel even under in gem. A nested worktree’s .git pointer file counts too.
It is matched by path component at any depth, and on both the written path and the one it resolves to, so a nested checkout’s git directory counts and a spelling chosen to dodge the check does not. A path crystl cannot resolve asks.
Only the automatic approval is withdrawn. A panel appears, you can approve it, and the write happens exactly as before. off and all are untouched: if you chose all, you chose it.
One plain invocation only
The safe-git list covers one plain git command and nothing else. A shell operator anywhere disqualifies the request whatever the subcommand: &, ||, ;, |, a backtick, $(, =(, >, <, or a newline. So git status | head asks, and git add . && rm -rf ~ never inherits git add’s approval.
Two further narrowings:
- No git global options before the subcommand.
git -c …,git -C …,git --git-dir=…,git --exec-path=…all ask, known flags and unknown alike. That is where the danger lives:--exec-pathrepoints the binaries git runs. - No side-effect flags anywhere.
--outputwrites a file, and--ext-diffand--textconvrun a configured external command.
One shape is carved out of the operator ban: git commit -m "$(cat <<'EOF' … EOF )", the standard commit idiom for agents writing a multi-line message without shell escaping. A quoted heredoc delimiter disables every expansion inside the body, so nothing in the message can run. The carve-out is exact rather than a loosening: an unquoted <<EOF gets no exemption, and any extra token anywhere in the command falls back to the general ban.
“In gem” describes where changes happen, not where every tool reaches. WebFetch and WebSearch still reach the internet. They alter nothing, and reads are what make the middle mode livable.
Best for: leaving a working agent alone without handing over the machine.
all
Every tool call is approved. Nothing waits.
Best for: a task you defined tightly, in a gem you are happy to hand over.
The gem is the ceiling
A permission mode declared inside your project may narrow what crystl auto-approves. It may never widen it.
Agents read permission settings out of the repo: a .claude/agents/*.md file can declare permissionMode: bypassPermissions, and a CLI flag can ask for the same thing. crystl used to honour that before it looked at your gem’s mode, so a file in the repo could auto-approve arbitrary commands under a gem you had deliberately set to the middle mode. Anyone who could write that file, including an agent with write access, had unattended command execution.
Now your gem’s mode wins:
- Gem off: nothing auto-approves, whatever the repo declares.
- Gem in gem:
bypassPermissionsanddontAskgrant nothing extra. The request gets exactly what in gem grants on its own.acceptEditsis narrower than the gem, so it still applies and limits auto-approval to edit-safe tools. - Gem all: everything auto-approves, including a declared bypass. That is your choice to make, and crystl honours it.
You may notice this as “crystl started asking me things.” If a repo you work in declares a bypass mode, work that used to run unattended under in gem now raises panels. That is the fix, not a regression. Set the gem to all if you want that repo’s declaration to hold.
Pause
pause auto-approvals in the crystl app menu is a global kill-switch. It is not a mode. It sits above every mode: per-gem, per-shard, and global settings all lose to it. Agents keep running, and every tool request escalates to a panel until you choose resume auto-approvals from the same menu.
Use it when you want to take the wheel back for a minute without editing any gem’s setting.
Approval cards
Each approval card is a floating glass panel that shows:
- The tool name (Edit, Bash, Read, etc.)
- The target (file path or command)
- A preview of what will change
- allow, always, and deny buttons, plus show › to jump to the shard
Panels are non-activating. They appear on screen without stealing focus from your current window. You can answer one and keep working.
No panel for the shard you’re watching
When crystl is focused and the requesting shard is on screen, the agent’s own approval menu is already in that terminal, so crystl doesn’t float a second copy of it over the top. Answer it in the terminal, or switch away and the panel appears. An approval that has no in-terminal menu always gets its panel, since that panel would be the only way to answer. Full rule: Panels you’re already watching.
Stop the whole session
deny rejects just the one tool call. Your agent keeps going and can try something else. To stop the entire session instead, hold Option while clicking deny. A confirmation appears first (“stop the whole session?”) so an accidental Option press can’t kill a run. There’s no separate button for this; it’s a deliberate power-user shortcut.
Allow all
When two or more approval requests are queued, an allow all bar appears, counting them, so you can batch-approve in one click. It acts only on requests that have a visible panel.
always
always approves this call and stops the agent asking again for that kind of call. crystl passes the agent’s own suggested permission rules back with the approval, written to its local settings (.claude/settings.local.json for Claude Code), so the rule belongs to the agent rather than to crystl. It is the panel equivalent of the agent’s own “don’t ask again”, not a change to your auto-approval mode.
Denial panels
When crystl denies a tool under in gem, an orange denial panel appears showing what was blocked. A retry button lets you override the auto-deny without switching modes, and dismiss clears the panel.
Per-gem auto-approval
You can override the global mode per gem. Open the gem menu (the ⋮ on the gem’s tab) and choose auto-approval › to set the mode directly, or settings for the full panel where AUTO-APPROVAL (CLAUDE / CODEX) sits on the general tab. Either way you get four options:
- Default (Global): use the global mode
- off: auto-approve nothing in this gem
- in gem: reads, in-tree changes, and safe git
- all: auto-approve everything in this gem
The per-gem setting is saved in .crystl/project.json and persists across sessions.
Priority chain
crystl resolves the active mode in this order:
- pause auto-approvals: the app-menu kill-switch. Nothing auto-approves while it is on.
- Session override: a temporary override supplied programmatically for an individual shard. The shard ⋮ menu does not expose this.
- Per-project config: the nearest
.crystl/project.jsonat or above the shard’s working directory, up to the gem root. Narrowest wins, so an isolated shard’s own worktree mode applies to that shard alone, and a sub-project youcdinto can set its own mode without the gem root overriding it. - Crystl session default: the fallback a quest or fanout launch sets for its own shards.
- Global mode: the default set on the agents settings page, defaults tab.
When you set a per-gem mode, the terminal prints a confirmation: ◆ Auto-approval changed from X to Y.
Global default
Set the global default in settings, agents > defaults, under AUTO-APPROVAL (DEFAULT). This is the fallback used when a gem has no override of its own. A gem-level mode always wins over it.
Upgrading from the old names
The modes used to be called Ask Every Time, Smart, and Auto. They are now off, in gem, and all. Same three modes, clearer names.
Nothing you have configured changes. The stored values are still manual, smart, and all, so an existing .crystl/project.json keeps working untouched, and a gem set to Smart is a gem set to in gem. There is nothing to migrate.
The CLI accepts both spellings with no deprecation warning. --approval smart and --approval in-gem produce the same shard, as do --approval manual and --approval off.
Which agents get cards
A card needs the agent to tell crystl about a tool call before it runs, and to accept an answer. Six agents do:
| Agent | Cards | What is different |
|---|---|---|
| Claude Code | yes | the full behaviour this page describes |
| Codex | yes | option+deny denies the one call; Codex has no session-stop verb |
| opencode | yes | cards start from opencode’s own permission event, so crystl never gates a tool opencode would have allowed. A held card expires after ten minutes and leaves opencode’s own prompt answerable |
| goose | yes, in auto mode |
goose resolves approvals before the hook runs, so in its approve and smart_approve modes a card would be a second prompt and crystl stays out of the way. auto is goose’s default and the only mode its headless form runs in. always behaves as allow-once |
| Kimi | yes | allow releases the hold rather than pre-approving, so Kimi may still prompt in its own terminal. The hold ends under ten minutes |
| GitHub Copilot CLI | yes | the one agent where allow fully replaces Copilot’s own prompt. No card at all under --allow-all-tools, --yolo, or COPILOT_ALLOW_ALL, because you asked for no prompting |
Two agents are not on that list. Antigravity keeps its approvals in its own terminal whatever mode you set, which is the one thing to plan around before leaving one running. aider reports no tool calls at all. For both, answer the prompts in the shard.
Two rules hold for every agent on that list. crystl never adds a gate the agent itself would not have asked about, and it never exceeds your own configuration: against Copilot’s --deny-tool, an allow is ignored. And failure is open and never quiet. If the bridge cannot be reached, or a card sits unanswered past its agent’s hook timeout, the tool runs and crystl raises a notification saying it stopped watching. A card worth waiting on is worth a glance rather than a walk away.
A worker’s approval goes to its orchestrator first
When a shard you fanned out hits an approval, the card does not land on you straight away. crystl dispatches one line into the lead shard’s terminal instead, naming the request and the two commands that decide it, and holds your card and your phone push back while it does. The lead’s ceiling is your own: it can decide only what you could have decided.
If the lead has not answered within 90 seconds, the ordinary card and push arrive exactly as they would have. Nothing is lost by the detour, and there is no extra notice to read.
On a free account there is no detour at all: deciding an approval from the CLI is a Guild command, so a worker’s approval comes straight to you with no delay. See CLI command tiers.
From the CLI
Approvals are also reachable via the crystl CLI. This helps when you’re triaging from another terminal, scripting a workflow, or running an agent that needs to coordinate with whoever holds the approval seat.
crystl pending # list every pending request
crystl approve 3 # approve request id 3
crystl deny 4 # deny request id 4
crystl wait pending --timeout 60 # block until the next one appears, then exit
crystl events --type pending_changed # stream approval lifecycle as JSON lines
crystl wait pending is built on the bridge’s SSE stream: no polling, exits cleanly on Ctrl-C, returns exit 1 on timeout.
Switching modes
You can change the mode at any time from the gem menu’s auto-approval › shortcut or the Gem Settings panel. The change takes effect immediately for pending and future requests in that gem. The individual shard ⋮ menu has no auto-approval setting.