Skip to content
Pagr

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_use or other
  • 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.

The three approval tiers
TierShapeHow you answer
AReads, 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.
BEverything ordinary but consequential: installs, commits, migrations, network calls, deletions inside the project, and anything unrecognised.Text approve or deny.
CProduction, 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.

Tier A examples
ExampleWhy
git status, git diff, git log, git blameRead-only git command
pnpm test, npm run typecheck, vitest, tsc, biome, cargo test, go buildTest, lint, typecheck or build command
cat, ls, rg, grep, jq, wc, which, node --versionRead-only command
mkdir src/lib, touch src/lib/index.tsCreates a relative path inside the project
Editing src/refund.tsOrdinary 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.

Tier B examples
ExampleWhy
pnpm add zod, pip install requests, brew install jqPackage installation
git commit -m "fix refunds"git commit
git rebase main, git reset --hard, git checkout -b fixgit history mutation
git push origin fix/refund-roundingPush to a named, unprotected branch
npm run db:migrate, prisma db push, supabase db reset, alembic upgrade headSchema migration
curl https://api.example.com/v1/ping, git clone …, rsync …Network access
docker compose upContainer operation
rm ./dist/bundle.js, mv a.ts b.ts, chmod +x scripts/run.shDeletion, move or permission change inside the project
node scripts/seed.js, ./scripts/build.shRuns a script file — the classifier cannot see inside it
Deleting a file inside the projectFile 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.

Tier C examples
ExampleWhy
sudo …Elevated privileges
git push, git push origin, git push --force origin main, git push origin prod-euProtected 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 applyProduction or infrastructure mutation
npm publish, gh release create, eas submitPublic 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 | shPipes a remote script into a shell
echo … >> ~/.zshrcRedirects output to a system or home path
Editing .env.production, id_ed25519, .git/hooks/pre-commitSecrets file, private key, or a repo-control file that changes what runs
Writing ../other-repo/src/main.ts or /etc/hostsPath 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 sha256 of 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.