Permission Policies
Control which agent tool calls auto-approve, which ask for permission, and how Pushary reaches you when they do
A policy decides what happens when your agent is about to run a tool: run it silently, ask you on your phone, ask in the terminal, or just notify you. Policies match a tool pattern: a bare tool name (Bash, Write, Edit), an exact argument (Bash(git status)), or a prefix (Bash(npm test:*)), with a * fallback. They only apply to agents that run through a Pushary hook; Supported agents lists which ones enforce.
Policies are evaluated by the hooks installed by npx @pushary/agent-hooks setup. Agents connected over MCP only (Windsurf, Claude Desktop, Cowork, ChatGPT, or Cursor without its hooks) send notifications and questions but don't gate tool calls, because there is no hook to enforce a policy.
The four approval modes
Every policy has a mode that decides where the approval happens.
| Mode | What the agent does | You are asked… |
|---|---|---|
push_first (When I'm out) | Asks your phone only when you are away from the agent's machine, or when Pushary cannot prove you are there. While you are at the Mac, the decision stays on the Mac however long it waits | On your phone when you are away, in the agent when you are present |
push_only (Every time) | Asks your phone for every approval, even while you are present, and blocks until you answer or the timeout fires | On your phone, every time |
terminal_only (Terminal) | Sends no approval alert | In the agent's own permission prompt |
notify_only (Updates) | Alerts your phone that approval is needed, with no approve or deny on the phone | In the agent's own permission prompt |
push_first (the default)
Shown as When I'm out. Pushary asks your phone only when it cannot place you at the agent's machine. Two signals place you there: the Pushary Mac app on that machine reports you attended (unlocked, awake, input in the last two minutes, not in a quiet scene), or a CLI agent hook on that machine recorded a prompt in the last five minutes and tool activity in the last two. Editor plugins report no presence of their own. While you are at the Mac the decision stays on the Mac, and time alone never sends it to your phone. Leave the Mac and it is released to your phone. Your phone is asked when neither signal holds for that machine, or when the question carries no machine id, as with Cowork and Claude Desktop. When a CLI or editor agent skips the phone because you are at the keyboard, the decision goes straight to the agent's own prompt. A Cursor file edit has no prompt of its own, so it is refused, and from Cursor plugin 0.2.3 or @pushary/agent-hooks 0.95.7 the refusal says to switch to Every time. When your phone is asked, the hook polls for up to pushFirstSeconds. Yes runs the call, No denies it, and no answer hands the decision back to the agent's own prompt.
push_only
Shown as Every time. Sends the push for every approval, even while you are at the machine, and blocks the agent until you answer or timeoutSeconds elapses. On a Mac with the Pushary app the local card still shows, and the first answer wins. On timeout it applies the policy's timeout action (see below). Use this for genuinely destructive operations where you want the agent to wait for an explicit human decision.
terminal_only
Shown as Terminal. No approval alert is sent, and no approval card opens on its own on the Mac (Pushary for Mac 0.1.35 or later; earlier versions can still open the card). The agent's own permission flow decides. Hosts with no permission prompt of their own refuse the action, as described under Updates below. Task updates are a separate setting and still arrive if you have them on. Use this for tools you always want to approve at the keyboard.
notify_only
Shown as Updates. Sends your phone a one-way alert (title "Agent needs approval", body = the tool description) with no approve or deny on the phone, and no approval card opens on its own on the Mac (Pushary for Mac 0.1.35 or later; earlier versions can still open the card). You decide in the agent's own permission prompt, and the agent never waits on Pushary. Hosts with no permission prompt of their own refuse the action. A Cursor file edit is denied, and from Cursor plugin 0.2.3 or @pushary/agent-hooks 0.95.7 the denial says why. The unattended Gemini bridge refuses with an explanation from @pushary/agent-hooks 0.95.7. Earlier versions pass the call to Gemini, which refuses it without saying why, because an unattended session treats a confirmation as a denial. Use this when you want awareness but not a remote gate.
If Pushary is unreachable, the hook returns no opinion, so the agent's own permission flow decides as if the hook were not installed. An offline agent can't reach your phone anyway, so it defers to the safest available approver: you, at the keyboard. A host with no prompt of its own, such as a Cursor file hook, refuses the action instead.
Policy fields
Prop
Type
The auto-approve shortcut
A policy with timeoutSeconds: 0 and timeoutAction: "approve" is allowed silently — no push, no terminal prompt, regardless of mode. This is how read-only tools stay frictionless, and it's exactly what Teach-on-Tap writes when you say "always allow."
The mirror image also holds: timeoutSeconds: 0 and timeoutAction: "deny" denies the call instantly, with no push and no terminal prompt. Combine it with a pattern like Bash(rm -rf:*) to hard-block a command family.
The one thing that overrides the auto-approve shortcut is the kill switch. A halted session is denied before the shortcut is checked, so even silently-approved tools stop.
Pattern syntax
The tool field accepts three pattern forms, modeled on Claude Code's own permission rules:
| Pattern | Matches | Example |
|---|---|---|
Tool | Every call to that tool | Bash |
Tool(exact arg) | Only calls whose argument equals the text exactly | Bash(git status) |
Tool(prefix:*) | Calls whose argument starts with the prefix | Bash(npm test:*) |
The argument under match is tool specific: command for Bash, file_path for Edit and Write. Other tools only match bare patterns, so an argument pattern for them never fires.
When several rules match one call, the most specific wins: an exact argument beats a prefix, a longer prefix beats a shorter one, a prefix beats the bare tool, and the bare tool beats * and the site defaults.
Prefix matching is plain string matching on the command, not shell parsing. Bash(git push:*) will not match cd repo && git push, and it matches any command that merely starts with the text. Claude Code's own permission rules share this property. Keep a safe bare-tool rule underneath your prefix rules.
Argument patterns are evaluated by the hook, so they need @pushary/agent-hooks 0.13.0 or later. On older versions an argument pattern simply never matches and the bare-tool and * rules still apply.
Default policies
Until you customize them, a new site uses these defaults (catch-all * plus a few well-known tools):
| Tool | Timeout | If no response | Mode | Push first |
|---|---|---|---|---|
Bash | 60s | Auto-deny | push_first | 20s |
Write | 60s | Ask in terminal | push_first | 20s |
Edit | 45s | Ask in terminal | push_first | 20s |
Read | 0s | Auto-approve | terminal_only | — |
* (default) | 60s | Ask in terminal | push_first | 20s |
Read ships with the auto-approve shortcut so file reads never interrupt you. Bash defaults to auto-deny on timeout — if you don't approve a shell command, it doesn't run.
Policies are cached locally for 5 minutes (in your temp dir as pushary-policy-<hash>.json), so editing a policy in the dashboard can take up to 5 minutes to take effect on a running agent, or restart the agent to pick it up immediately.
Editing policies in the dashboard
Open the policy editor
Go to Dashboard → Agent → Policies. Each row is one tool pattern.
Set the mode and timeouts
For each tool choose a Mode, a Timeout (0 to 300s), and an If no response action (Auto-approve / Auto-deny / Ask in terminal). The Push window field is only editable when the mode is push_first.
Add a rule for a specific tool
Type a pattern (e.g. Bash, Bash(git status), or Bash(npm test:*)) and Add rule to override the * default. The * row is the catch-all and can't be deleted.
From the command line
The pushary mode command sets a temporary, site-wide mode override that wins over per-tool modes until it expires:
pushary mode push_only --for 30m # force push_only for 30 minutes
pushary mode status # show the current override
pushary mode clear # back to per-tool policiesDurations are <n>m or <n>h. The override is always TTL'd server-side, so a forgotten override self-heals.
Teach-on-Tap: "always allow this tool"
When a push question is about a specific tool, the decision page shows a button: "Always allow <tool> — stop asking." Tapping it approves the current call and mints a durable policy for that tool so you're never asked again.
Under the hood it writes a policy with timeoutSeconds: 0, timeoutAction: "approve", mode: "push_only", pushFirstSeconds: 0 — which trips the auto-approve shortcut on every future call.
Teach-on-Tap is scoped per site and per exact tool name (e.g. Bash). It is not scoped to one agent or one project — once you always-allow Bash, every agent on that site auto-runs Bash. Reverse it any time by editing or deleting the rule in the policy editor.