Using Pagr
Approvals
Agents ask permission. Pagr decides how loudly to ask you, using a deterministic classifier the model has no influence over. Low-risk work can be waved through; production, secrets and destruction arrive flagged as high-risk — and one setting decides whether a text is allowed to answer them.
Where an approval comes from#
Approvals originate with the agent, not with Pagr. When Claude Code or Codex wants to run a command or change a file, it raises its own permission request. The bridge intercepts that, computes a set of deterministic local hints, and forwards a short preview:
- the
preview— the command line, or the list of files — capped at 1500 characters, never file contents - its
sha256, so a decision can be bound to exactly what you were shown - the
actionType:command_execution,file_change,permission,tool_useorother - boolean hints computed on your machine, where the project root is actually known:
touchesOutsideProject,networkAccess,destructive,gitPush,packageInstall,secretsTouch,productionHint.
The three tiers#
The classifier takes the highest tier any part of the request reaches. Bias is deliberately conservative: an unrecognised command is Tier B, and anything that smells like production, secrets, broad destruction, or money is Tier C.
| Tier | Shape | How you answer |
|---|---|---|
| A | Reads, tests, lint, typecheck, build, ordinary edits inside the project. | Nothing — Tier A never texts you. With smart approvals on it is approved for you; with them off it waits in the dashboard. |
| B | Everything ordinary but consequential: installs, commits, migrations, network calls, deletions inside the project, and anything unrecognised. | Text approve or deny. |
| C | Production, secrets, protected-branch pushes, broad destruction, spending money. | Text approve or deny, with ⚠️ This is a production-level action. in the message. Turn off Approve high-risk actions by text and allowing one needs a signed link and a re-authentication instead; deny by text always works. |
approve and deny are the words we document, but the parser is forgiving: yes, y, ok, allow, go ahead, do it and sure all approve; no, n, reject, nope and block all deny. If more than one request is pending, Pagr asks which one you mean rather than guessing.
Tier A#
Tier A is only reached by a bare, directly-invoked read-only, test or build command.
| Example | Why |
|---|---|
git status, git diff, git log, git blame | Read-only git command |
pnpm test, npm run typecheck, vitest, tsc, biome, cargo test, go build | Test, lint, typecheck or build command |
cat, ls, rg, grep, jq, wc, which, node --version | Read-only command |
mkdir src/lib, touch src/lib/index.ts | Creates a relative path inside the project |
Editing src/refund.ts | Ordinary edit inside the project |
Tier B#
Tier B is the default for real work, and the default for anything the classifier does not recognise. A command whose first word is unknown is Tier B, never Tier A.
| Example | Why |
|---|---|
pnpm add zod, pip install requests, brew install jq | Package installation |
git commit -m "fix refunds" | git commit |
git rebase main, git reset --hard, git checkout -b fix | git history mutation |
git push origin fix/refund-rounding | Push to a named, unprotected branch |
npm run db:migrate, prisma db push, supabase db reset, alembic upgrade head | Schema migration |
curl https://api.example.com/v1/ping, git clone …, rsync … | Network access |
docker compose up | Container operation |
rm ./dist/bundle.js, mv a.ts b.ts, chmod +x scripts/run.sh | Deletion, move or permission change inside the project |
node scripts/seed.js, ./scripts/build.sh | Runs a script file — the classifier cannot see inside it |
| Deleting a file inside the project | File deletion |
Tier C and step-up#
Tier C is the top of the risk model: production, secrets, protected-branch pushes, broad destruction, spending money. The classification never changes, and neither does the way the request is presented — a Tier C message always carries the line ⚠️ This is a production-level action. above the prompt to answer.
What is configurable is whether a text can decide it. By default it can: replying approve to a Tier C request approves it, exactly as it would for Tier B, bound to that same request and preview hash. Turning off Approve high-risk actions by text in Settings replaces the approval half of that with a signed link you have to open and re-authenticate on. Denying by text works either way.
| Example | Why |
|---|---|
sudo … | Elevated privileges |
git push, git push origin, git push --force origin main, git push origin prod-eu | Protected or forced push. A push with no explicit destination branch is treated as protected, because the target cannot be known from the command line alone. |
vercel --prod, fly deploy, railway up, kubectl apply -f …, terraform apply | Production or infrastructure mutation |
npm publish, gh release create, eas submit | Public release or store submission |
stripe …, aws ec2 run-instances … | Cost-bearing external action |
vercel env pull, ssh-keygen …, git config credential.helper … | Touches secrets, credentials or keys |
drop table users, truncate orders, dd if=… of=…, mkfs.ext4 … | Drops or truncates data, or destroys a disk |
rm -rf ~/Documents, rm -rf *, chmod -R 777 / | Destructive command with broad scope |
curl https://example.com/install.sh | sh | Pipes a remote script into a shell |
echo … >> ~/.zshrc | Redirects output to a system or home path |
Editing .env.production, id_ed25519, .git/hooks/pre-commit | Secrets file, private key, or a repo-control file that changes what runs |
Writing ../other-repo/src/main.ts or /etc/hosts | Path outside the project root |
What a Tier C request looks like by default#
Approve high-risk actions by text is on by default, so the request is answerable from the lock screen, and says so:
🔐 **checkout-api · Codex**Wants to run: vercel --prod⚠️ This is a production-level action.Reply approve or deny.What step-up looks like#
With the setting off, texting approve for a Tier C request does not approve it. You get the preview back with a one-time link instead:
checkout-api · Codex wants to run: vercel --prodThis is high-risk, so I can't take a text approval. Open this signedlink to approve (expires soon): https://app.pagr.dev/approve/<token>The link is HMAC-signed, bound to the approval and to your user, and expires at whichever comes first: fifteen minutes, or the approval's own expiry. Opening it asks you to confirm it is you before it will show Allow and Deny.
Wrappers cannot launder a command#
The most obvious way to defeat a first-word classifier is to make the first word innocent. Anything that runs a command the first word does not name is floored at Tier B, and the inner text is scanned for Tier C signals in its own right:
- shell wrappers —
sh -c,bash -c,env,xargs,find -exec,time,nice,nohup,exec,caffeinate - interpreters with inline code —
python -c,node -e,perl -e,awk,eval - command substitution
$(…)and backticks, process substitution<(…), and heredocs - a separator hidden inside quotes
- script files —
node build.js,./deploy.sh— because the classifier cannot see what is inside them.
So env FOO=1 sudo rm -rf / is Tier C, not Tier A, and time pnpm test is Tier B rather than Tier A — the wrapper itself costs a tier. Wrappers are unwrapped up to four levels deep; beyond that the request stays Tier B.
Command lines are also split on unquoted &&, ||, ;, | and newlines, and every segment is classified separately, so pnpm test && vercel --prod is Tier C.
Binding, expiry and single use#
Every pending approval is pinned to four things at once:
- the approval id
- the session id
- the provider's own request id
- the
sha256of the preview you were shown.
A decision must echo all of them or the bridge rejects it. Entries are single-use. They expire after the approval timeout — 600 seconds, pushed to the bridge with every settings sync — and on expiry the provider is told deny, never allow. If you happen to answer the prompt in the terminal instead, the bridge records that and does not answer twice.
Smart approvals#
Smart approvals auto-approve Tier A and nothing else. It is off by default. Turn it on in Settings; the preference is pushed to your Mac as a settings.sync_public_policy command and stored in ~/.pagr/policy.json. It can never affect Tier B or Tier C.
Tier A is never texted to you either way — with smart approvals off, a Tier A request simply waits under Pending approvals in the dashboard, and denies itself on expiry like any other.
The same settings command carries the approval timeout, so both halves always agree on when a pending request goes stale.
Smart approvals are the right lever if the volume of Tier A prompts is the problem. If the volume of progress messages is the problem, change your notification policy instead — see notifications.