Skip to main content

Permissions API

Manage permission requests for agent tool calls using a tiered classification system. Commands are classified at runtime into three tiers:
  • Safe — auto-approved (read-only operations like ls, cat, git status)
  • Dangerous — routed to the dashboard for user approval (code execution, network writes, git push)
  • Destructive — blocked by default (rm -rf /, DROP TABLE, terraform destroy)
This is Phase 1 of sandbox governance. Safe commands pass through automatically. Dangerous commands are queued for approval and can be reviewed in the dashboard. Destructive commands are blocked and require explicit user enablement via the dashboard.

List pending permission requests

GET /api/permissions
Returns all pending permission requests for the authenticated user. You can optionally filter by agent. Requires bearer token authentication.

Query parameters

ParameterTypeRequiredDescription
agentIdstringNoFilter pending requests by agent ID. When omitted, returns all pending requests for the authenticated user.

Response

{
  "pending": [
    {
      "id": "perm_1711929600000_a1b2c3d4e",
      "agentId": "agent_123",
      "userId": "user_456",
      "toolName": "bash",
      "toolInput": {
        "command": "node script.js"
      },
      "tier": "dangerous",
      "reason": "Dangerous command: ^node\\s",
      "timestamp": 1711929600000,
      "status": "pending"
    }
  ],
  "message": "Permission system initialized. Pending requests will appear here."
}
FieldTypeDescription
pendingarrayList of pending permission requests
messagestringHuman-readable status message about the permission system
pending[].idstringUnique request identifier (format: perm_{timestamp}_{random})
pending[].agentIdstringAgent that triggered the tool call
pending[].userIdstringOwner of the agent
pending[].toolNamestringName of the tool being invoked (for example, bash, write, read)
pending[].toolInputobjectInput parameters passed to the tool
pending[].tierstringClassification tier: safe, dangerous, or destructive
pending[].reasonstringHuman-readable explanation of why the command was classified at this tier
pending[].timestampnumberUnix timestamp (milliseconds) when the request was created
pending[].statusstringRequest status: pending, approved, or rejected

Errors

CodeDescription
401Unauthorized — missing or invalid bearer token
403Forbidden

Submit permission decision

POST /api/permissions
Approve or reject a pending permission request. Requires bearer token authentication.

Request body

FieldTypeRequiredDescription
requestIdstringYesThe id of the pending permission request
decisionstringYesOne of: approve, reject, approve_always
feedbackstringNoOptional reviewer feedback or notes to attach to the decision
modifiedInputstringNoOptional modified command input. When provided, the approved tool call executes with this value instead of the original input.
The approve_always decision approves the current request and is intended to auto-approve similar commands in the future. Auto-approval persistence is not yet implemented — currently approve_always behaves the same as approve.

Response

{
  "success": true,
  "requestId": "perm_1711929600000_a1b2c3d4e",
  "decision": "approve"
}
FieldTypeDescription
successbooleanWhether the decision was processed
requestIdstringThe request that was decided
decisionstringThe decision that was applied

Errors

CodeDescription
400Missing requestId or decision
400Invalid decision value. Must be one of: approve, reject, approve_always.
401Unauthorized — missing or invalid bearer token
403Forbidden
404Request not found — the requestId does not match any pending request

Command classification tiers

The classifier evaluates commands and tool calls against built-in pattern lists. Unknown commands default to the dangerous tier.

Safe tier (auto-approve)

Commands that are read-only or informational:
CategoryExamples
Filesystem readcat, head, tail, ls, find, stat, wc, du, df
Text processinggrep, sort, uniq, cut, awk, sed
Git read-onlygit status, git diff, git log, git show, git branch
System infoecho, pwd, whoami, date, uptime, env
Network read-onlycurl (GET), wget, ping, nslookup, dig
Package infonpm list, npm view, pip list, pip show
Docker read-onlydocker ps, docker images, docker logs, docker inspect

Dangerous tier (requires approval)

Commands that modify state or execute code:
CategoryExamples
Code executionpython, node, npx, npm run, npm install
Network writescurl -X POST, curl -d, wget --post
Git writesgit push, git commit, git merge, git rebase, git reset
Container writesdocker run, docker build, docker exec, docker rm
Remote executionssh, scp, rsync
File writesRedirects to /, mv /, cp ... /
Infrastructurerailway up, vercel deploy

Destructive tier (blocked by default)

Commands that can cause irreversible damage:
CategoryExamples
Filesystem destructionrm -rf /, sudo rm, dd if=, mkfs, fdisk
Repository destructiongh repo delete, gh repo edit --visibility public
Database destructionDROP DATABASE, DROP TABLE, TRUNCATE, DELETE FROM
Infrastructure destructionterraform destroy, railway service delete, docker system prune
System modificationsudo, chmod 777, chown

Tool call classification

In addition to shell commands, the classifier handles structured tool calls:
Tool nameClassification
bash, exec, shellClassified based on the command parameter using the rules above
write, file_writedangerous if writing to sensitive paths (.env, credentials, .ssh); otherwise safe
read, file_readAlways safe
Unknown toolsDefault to dangerous

WebSocket real-time notifications

Instead of polling GET /api/permissions, you can connect via WebSocket to receive instant push notifications when a permission request is created. The WebSocket endpoint replaces the previous 5-second polling approach with real-time delivery.
ws://HOST/ws/permissions?userId=USER_ID

Connection

Connect by passing your userId as a query parameter. On successful connection, the server sends a connected message:
{
  "type": "connected",
  "data": { "userId": "user_456", "timestamp": 1711929600000 }
}
If the userId parameter is missing, the server closes the connection with code 4001.

Server-to-client messages

Message typeDescriptionData fields
connectedSent on successful connectionuserId, timestamp
permission_requestA new permission request requires approvalid, command, tier, reason, agentId
decision_ackConfirms that a decision was processedrequestId, decision, timestamp
heartbeatSent every 30 seconds to keep the connection alivetimestamp
pongResponse to a client pingtimestamp
errorSent when the server cannot parse a client messagemessage

Permission request example

{
  "type": "permission_request",
  "data": {
    "id": "hook_1711929600000_a1b2c3d4e",
    "command": "node script.js",
    "tier": "dangerous",
    "reason": "Dangerous command: ^node\\s",
    "agentId": "agent_123"
  }
}

Client-to-server messages

Message typeDescriptionData fields
decisionSubmit an approval or rejection for a pending requestrequestId, decision
pingConnectivity check; server responds with pong(none)

Decision example

{
  "type": "decision",
  "data": {
    "requestId": "hook_1711929600000_a1b2c3d4e",
    "decision": "approve"
  }
}
The decision field accepts approve or reject.

Connection lifecycle

  • Heartbeat — the server sends a heartbeat every 30 seconds. If you do not receive a heartbeat within the expected interval, reconnect.
  • Cleanup — the server removes the client from its tracking map on disconnect. No explicit close handshake is required beyond the standard WebSocket close frame.
  • Multiple sessions — a single user can have multiple concurrent WebSocket connections. All sessions for a user receive the same permission_request broadcasts.
The REST API (GET /api/permissions and POST /api/permissions) remains fully supported. The WebSocket endpoint is an alternative real-time channel. If the WebSocket connection drops, you can fall back to polling the REST endpoint.

Pre-tool-use hook

The permission system integrates with Docker agent containers through a pre-tool-use hook. When an agent makes a tool call inside its container:
  1. The --hook-pre-tool-use flag triggers the hook script
  2. The hook script sends tool details to POST /api/hooks/classify
  3. The classify endpoint evaluates the tool name and input
  4. Safe tools are auto-approved — the agent proceeds immediately
  5. Dangerous tools are queued — the server pushes a permission_request message via the WebSocket endpoint (or the dashboard can poll GET /api/permissions)
  6. Destructive tools are blocked — the agent cannot proceed
The hook system is fail-closed. If the classify endpoint is unreachable, all tool calls are denied by default. See the hooks classify API for full endpoint details including request format and authentication.
{
  "allow": false,
  "tier": "dangerous",
  "reason": "Queued for approval: Dangerous command: ^node\\s",
  "requestId": "hook_1711929600000_a1b2c3d4e"
}
FieldTypeDescription
allowbooleanWhether the tool call was permitted
tierstringClassification tier
reasonstringExplanation of the classification
requestIdstringPresent only for dangerous tier. Use this ID to approve or reject the request via POST /api/permissions or the WebSocket decision message.