
An agent that can send emails, delete files, or push to production needs a human in the loop for high-risk actions. The useful question is which actions are high-risk. Answer it once with a tool-risk decision table, then reference it every time you add a new tool.
The Figment virtual Intel macOS environment at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) is a capable host for orchestration agents that call external APIs, manage files, and trigger CI pipelines. It runs macOS, reaches outbound to any approval channel or API, and runs the terminal-based approval scripts below. Access is through the browser console; run the agent from the desktop terminal, or deploy an outbound GitHub Actions self-hosted runner that the agent job targets.
Move up to an M-series Mac from $95/month when the agent invokes ARM-only binaries or macOS-native frameworks as part of an action. Exact chip, RAM and disk are confirmed in the quote. The Managed Build Mac at $249/month bundles one M-series Mac with CI setup, backups, monitoring, and 30 minutes of monthly support — useful when the approval gate lives inside a CI workflow and pull-request reviewers are already the operators. Physical plans collect requests until supply is confirmed; ask for a configuration before you commit.
Before adding a tool to your agent, classify it here. The classification determines whether the action runs automatically, needs a soft confirmation, or needs an explicit human approval before proceeding.
| Tool / action | Reversible? | Blast radius | Risk level | Approval required |
|---|---|---|---|---|
| Read a file or directory listing | Yes | None | Low | No — run automatically |
| Fetch data from a read-only API | Yes | None | Low | No — run automatically |
| Write or update a local file | Partial (if backed up) | Single file | Medium | Soft confirm (log + continue unless --dry-run) |
| Create or update a database record | Partial (if snapshotted) | Single record | Medium | Soft confirm |
| Send an email or Slack message | No — message is delivered | Recipient inbox | High | Human approval required |
| Delete a file or database record | No (without backup) | Single item | High | Human approval required |
| Execute a shell command built from agent output | No | Arbitrary | Critical | Human approval + command displayed before execution |
| Push code to a production branch | No | Production system | Critical | Human approval required |
| Call an external API that bills per request | Partial (request sent; can refund) | Billing, side effects | High | Soft confirm with per-job budget guard |
| Revoke or rotate a credential | Partial (re-issue) | All services using it | Critical | Human approval required |
Revisit this table when you add any new tool. If a tool's blast radius is unclear, treat it as Critical until you understand the downstream effects.
If your agent runs as a GitHub Actions job on a Figment self-hosted runner, use a protected environment with required reviewers, documented in GitHub's managing environments guide, to gate the high-risk phase. Pair it with GitHub's hardening guidance for self-hosted runners — self-hosted runners are not ephemeral by default and should only run trusted code.
# .github/workflows/agent-with-approval.yml
jobs:
plan:
runs-on: [self-hosted, macOS]
outputs:
action_plan: ${{ steps.plan.outputs.plan }}
steps:
- id: plan
run: python3 agent_planner.py --output plan.json
- run: cat plan.json # Reviewers see the planned actions
execute:
needs: plan
runs-on: [self-hosted, macOS]
environment: production-approval # requires a named reviewer to approve
steps:
- run: python3 agent_executor.py --plan plan.json
Configure production-approval in Settings → Environments: add required reviewers (up to six), optionally prevent self-approval, and set a wait timer appropriate for your team. Environment-specific secrets (API keys, deploy tokens) are injected only after approval. When execute is reached, GitHub pauses the workflow and notifies reviewers, who see the plan.json output from the previous step before approving. The exact option names move around in GitHub's UI; check the linked guide against what you see.
For agents launched directly on the Mac through launchd rather than GitHub Actions, an approval gate script pauses execution and waits for a signal file before continuing.
#!/bin/bash
# approval_gate.sh
# Usage: ./approval_gate.sh "ACTION_DESCRIPTION" TIMEOUT_SECONDS
set -euo pipefail
DESCRIPTION="${1:-Approve action}"
TIMEOUT="${2:-300}"
GATE_DIR="/Users/agent/approvals"
LOG="/Users/agent/logs/approvals.log"
mkdir -p "$GATE_DIR" "$(dirname "$LOG")"
TOKEN="$(date +%s)_$$_$RANDOM"
APPROVE_FILE="$GATE_DIR/approve_${TOKEN}"
DENY_FILE="$GATE_DIR/deny_${TOKEN}"
echo "$(date -u +%FT%TZ) APPROVAL REQUIRED: $DESCRIPTION" | tee -a "$LOG"
echo " -> touch '$APPROVE_FILE' to approve"
echo " -> touch '$DENY_FILE' to deny"
# Notify the operator via a Shortcut you have pre-built in Shortcuts.app.
shortcuts run "Alert: Approval Required" 2>/dev/null || true
elapsed=0
while [ "$elapsed" -lt "$TIMEOUT" ]; do
if [ -f "$APPROVE_FILE" ]; then
echo "$(date -u +%FT%TZ) APPROVED: $DESCRIPTION" >> "$LOG"
rm -f "$APPROVE_FILE" "$DENY_FILE"
exit 0
fi
if [ -f "$DENY_FILE" ]; then
echo "$(date -u +%FT%TZ) DENIED: $DESCRIPTION" >> "$LOG"
rm -f "$APPROVE_FILE" "$DENY_FILE"
exit 1
fi
sleep 5
elapsed=$((elapsed + 5))
done
echo "$(date -u +%FT%TZ) TIMEOUT: $DESCRIPTION" >> "$LOG"
exit 1
The macOS Shortcuts command line runs a Shortcut you build once in Shortcuts.app. shortcuts run "Alert: Approval Required" sends a notification; the operator responds by touching the approve or deny file from any terminal on the VM. Inject the gate into the agent loop for every High and Critical action:
import subprocess
def require_approval(description: str, timeout: int = 300) -> None:
result = subprocess.run(
["/Users/agent/scripts/approval_gate.sh", description, str(timeout)],
check=False,
)
if result.returncode != 0:
raise RuntimeError(f"Action denied or timed out: {description}")
| Outcome | Agent behaviour | Logged |
|---|---|---|
| Approved within timeout | Execution continues | Yes — APPROVED with timestamp |
| Denied by operator | RuntimeError raised, exit 1 | Yes — DENIED with timestamp |
| Timeout expires | Exit 1; launchd retries on next interval | Yes — TIMEOUT with description |
| Operator unreachable | Timeout fires after configured seconds | Yes — treated as timeout |
Do not set timeouts longer than your acceptable blast-radius window. A "delete database records" action with a 24-hour approval window defers damage, it does not prevent it.
Every approval decision must be durable:
approvals.log monthly and retain at least 90 days.The $29 Figment VM runs both patterns above. Start with the GitHub Actions environment gate if your agent already has a CI workflow; use the local gate script for launchd-scheduled agents. If you need ARM execution or GPU work alongside the approval logic, request an M-series quote. Browse Mac hosting plans or request your configuration.
Related reading