HaltDashboard

How Halt works

Halt sits outside your agents. Before each tool call, the agent's harness asks Halt whether it may proceed; a supervisor process can also freeze or kill the agent at the OS level. Neither depends on the model choosing to obey.

States

running: calls are checked against breakers and tripwires. paused: every call is denied and supervised processes are frozen (SIGSTOP); resuming thaws them exactly where they were. stopped: every call is denied and supervised processes are killed. STOP ALL is a separate account-wide flag on top of these; lifting it leaves individually paused or stopped agents as they were. Halt never restarts a killed process. Exact semantics

Breakers (defaults)

  • Mass deletion: more than 20 in 5 min → pause
  • Outbound message burst: more than 15 in 10 min → pause
  • Money moves: more than 5 in 1h → pause
  • Runaway loop: more than 400 in 10 min → pause
  • Tamper protection: always denied
  • Catastrophic shell commands: always denied

Tripwires

Canary secrets are checked inside every tool call (including request bodies); canary links trip on any fetch. Both stop the agent (or everything, when not linked to one) and send an urgent alert.

For agents (no human in the loop)

Everything in the dashboard is available to agents: POST https://aiapp-halt.vercel.app/api/v1/accounts creates an account and returns the owner key; every other operation is POST https://aiapp-halt.vercel.app/api/v1/owner/<operation> with Authorization: Bearer <owner key>, or the same tools over MCP at https://aiapp-halt.vercel.app/api/mcp. The full list with JSON Schemas is at /api/v1/ops and the agent guide at /skill.md.

The supervising party keeps the owner key; supervised agents only ever get their own token. For unattended fleets, give breakers a cooldown so a tripped agent resumes on its own, and have an overseer consume alarms via wait_for_alarms or a signed webhook (x-halt-signature = HMAC-SHA256 of the body with the alert key).

Claude Code

1. Give Claude Code the token

export HALT_TOKEN=hat_your_agent_token

Put it in your shell profile so every session has it.

2. Add the hook (~/.claude/settings.json or .claude/settings.json)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "http",
            "url": "https://aiapp-halt.vercel.app/api/v1/hooks/claude-code",
            "timeout": 10,
            "headers": {
              "Authorization": "Bearer $HALT_TOKEN"
            },
            "allowedEnvVars": [
              "HALT_TOKEN"
            ]
          }
        ]
      }
    ]
  }
}

Merge into your existing hooks. Every tool call is checked before it runs; Halt only ever answers "deny" or nothing, so your normal permission prompts stay exactly as they are.

3. Optional: hard stop for the whole session

curl -fsSLO https://aiapp-halt.vercel.app/halt.mjs
HALT_URL=https://aiapp-halt.vercel.app HALT_SUPERVISOR_TOKEN=hsv_your_supervisor_token node halt.mjs run -- claude

Runs Claude Code under the Halt supervisor: Pause freezes the process, Stop kills it, even mid-command.

Hardened setup for unattended agents (fail-closed, tamper-proof)

Managed settings: /etc/claude-code/managed-settings.json (macOS: /Library/Application Support/ClaudeCode/managed-settings.json)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "http",
            "url": "https://aiapp-halt.vercel.app/api/v1/hooks/claude-code",
            "timeout": 10,
            "headers": {
              "Authorization": "Bearer $HALT_TOKEN"
            },
            "allowedEnvVars": [
              "HALT_TOKEN"
            ],
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

Root-owned, so the agent can't edit it, and a user or project disableAllHooks can't turn it off. "onFailure": "block" (Claude Code 2.1.295+) blocks every tool call while Halt is unreachable, times out or errors. Details: security.

OpenClaw

1. Install the Halt plugin

mkdir -p halt-openclaw && cd halt-openclaw
for f in package.json openclaw.plugin.json index.ts; do curl -fsSLO https://aiapp-halt.vercel.app/openclaw/$f; done
cd .. && openclaw plugins install --link ./halt-openclaw --force && openclaw plugins enable halt

Review the three files first: they are short. Native plugins run inside the Gateway.

2. Configure it (openclaw.json → plugins.entries.halt)

{
  "plugins": {
    "entries": {
      "halt": {
        "enabled": true,
        "config": {
          "url": "https://aiapp-halt.vercel.app",
          "token": "hat_your_agent_token"
        }
      }
    }
  }
}

Every tool call goes through before_tool_call; Halt blocks it when you pressed STOP, a breaker tripped or a tripwire fired. Add "failClosed": true to block when Halt is unreachable.

3. Recommended: supervise the gateway

curl -fsSLO https://aiapp-halt.vercel.app/halt.mjs
HALT_URL=https://aiapp-halt.vercel.app HALT_SUPERVISOR_TOKEN=hsv_your_supervisor_token node halt.mjs run -- openclaw gateway

A hook can't cancel work already in flight. The supervisor can: Pause freezes the gateway's process tree, Stop kills it.

Any agent process

Run any agent under the Halt supervisor

curl -fsSLO https://aiapp-halt.vercel.app/halt.mjs
HALT_URL=https://aiapp-halt.vercel.app HALT_SUPERVISOR_TOKEN=hsv_your_supervisor_token node halt.mjs run -- <your agent command>

No code changes. The supervisor polls Halt every 2s: Pause freezes the process tree (SIGSTOP), Resume thaws it, Stop or STOP ALL terminates it. Add --fail-closed (and --grace <seconds>, default 60) to stop the agent if Halt becomes unreachable. Run as root with --as <user> so the agent can't kill the supervisor.

Containers: a hard boundary

HALT_URL=https://aiapp-halt.vercel.app HALT_SUPERVISOR_TOKEN=hsv_your_supervisor_token node halt.mjs watch \
  --on-pause "docker pause my-agent" --on-resume "docker unpause my-agent" --on-stop "docker kill my-agent"

docker pause/kill act on the container's cgroup, so no child process can slip out. Add --on-start to restart a stopped agent when you set it back to running; without it, nothing is restarted.

Optional: gate individual actions from shell scripts

export HALT_URL=https://aiapp-halt.vercel.app
export HALT_TOKEN=hat_your_agent_token
node halt.mjs check send_email "to: all-staff@company.com" || exit 1

Exit code 0 = allowed, 2 = denied (reason on stderr).

Your own code

Check before each action (HTTP)

curl -s https://aiapp-halt.vercel.app/api/v1/check \
  -H "authorization: Bearer hat_your_agent_token" -H 'content-type: application/json' \
  -d '{"tool":"send_email","input":{"to":"bob@example.com"}}'
# → {"decision":"allow"|"deny","state":"running"|"paused"|"stopped","reason":...}

Python

import os, requests

def halt(tool, **input):
    r = requests.post("https://aiapp-halt.vercel.app/api/v1/check", timeout=5,
        headers={"authorization": f"Bearer {os.environ['HALT_TOKEN']}"},
        json={"tool": tool, "input": input}).json()
    if r["decision"] != "allow":
        raise SystemExit(f"Halted: {r['reason']}")

halt("delete_file", path="/data/report.csv")  # call before every side effect

TypeScript

async function halt(tool: string, input: unknown) {
  const r = await fetch("https://aiapp-halt.vercel.app/api/v1/check", {
    method: "POST",
    headers: { authorization: `Bearer ${process.env.HALT_TOKEN}`, "content-type": "application/json" },
    body: JSON.stringify({ tool, input }),
  }).then((r) => r.json());
  if (r.decision !== "allow") throw new Error(`Halted: ${r.reason}`);
}

API

Agent endpoints take the agent token as Authorization: Bearer hat_….

POST/api/v1/checkBefore an action: {tool, input?, summary?} → {decision: allow|deny, state, stop_all, reason}
POST/api/v1/hooks/claude-codeClaude Code PreToolUse HTTP hook (empty 200 = no objection; JSON deny = blocked)
POST/api/v1/hooks/openclawOpenClaw plugin endpoint: {toolName, params} → {block, blockReason}
POST/api/v1/heartbeatSupervisor poll (supervisor token): → {state, stop_all, reason, poll_ms}; {event: "exit"} = clean shutdown
POST/api/v1/connecthalt.mjs connect: one-time code → fresh agent + supervisor tokens
POST/api/v1/act/{alert-key}/stop-allSTOP ALL (used by notification buttons)
POST/api/v1/act/{alert-key}/resume-allLift STOP ALL, only if you allowed resume from alerts
POST/api/v1/act/{alert-key}/agents/{id}/{pause|resume|stop}Per-agent control (resume only if allowed)
GET/api/cron/sweepSilent-supervisor alarms + pruning; call every minute with Authorization: Bearer $CRON_SECRET
POST/api/v1/feedbackFeedback from people or agents: {message, kind?, contact?}

Failure modes, honestly