curl --request PUT \
--url https://api.opal.dev/v1/agent-kill-switch \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"active": true,
"duration_minutes": 123,
"reason": "<string>"
}
'import requests
url = "https://api.opal.dev/v1/agent-kill-switch"
payload = {
"active": True,
"duration_minutes": 123,
"reason": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({active: true, duration_minutes: 123, reason: '<string>'})
};
fetch('https://api.opal.dev/v1/agent-kill-switch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.opal.dev/v1/agent-kill-switch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'active' => true,
'duration_minutes' => 123,
'reason' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.opal.dev/v1/agent-kill-switch"
payload := strings.NewReader("{\n \"active\": true,\n \"duration_minutes\": 123,\n \"reason\": \"<string>\"\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.opal.dev/v1/agent-kill-switch")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"active\": true,\n \"duration_minutes\": 123,\n \"reason\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.opal.dev/v1/agent-kill-switch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"active\": true,\n \"duration_minutes\": 123,\n \"reason\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"active": true,
"activated_at": "2023-11-07T05:31:56Z",
"activated_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"expires_at": "2023-11-07T05:31:56Z",
"deactivated_at": "2023-11-07T05:31:56Z",
"reason": "<string>"
}Set the agent kill switch
Prototype (AG-297). Turns the org-level agent kill switch on or off. Admin only.
While it is on, Opal answers every PreToolUse hook POST to /agent-activity with a deny decision, so a cooperating Claude Code client stops running tools.
It is not a security boundary, and it fails open. It reaches only clients that are configured to POST to /agent-activity and that honour the answer; a client on a non-default base URL, an older build, or with no hook deployed is unaffected. If Opal is unreachable or slow the tool call proceeds, because Claude Code treats a failed PreToolUse hook as a non-blocking error by design.
Activation is always time-bound: duration_minutes is capped, and an activation that reaches expires_at stops denying on its own.
curl --request PUT \
--url https://api.opal.dev/v1/agent-kill-switch \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"active": true,
"duration_minutes": 123,
"reason": "<string>"
}
'import requests
url = "https://api.opal.dev/v1/agent-kill-switch"
payload = {
"active": True,
"duration_minutes": 123,
"reason": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({active: true, duration_minutes: 123, reason: '<string>'})
};
fetch('https://api.opal.dev/v1/agent-kill-switch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.opal.dev/v1/agent-kill-switch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'active' => true,
'duration_minutes' => 123,
'reason' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.opal.dev/v1/agent-kill-switch"
payload := strings.NewReader("{\n \"active\": true,\n \"duration_minutes\": 123,\n \"reason\": \"<string>\"\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.opal.dev/v1/agent-kill-switch")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"active\": true,\n \"duration_minutes\": 123,\n \"reason\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.opal.dev/v1/agent-kill-switch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"active\": true,\n \"duration_minutes\": 123,\n \"reason\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"active": true,
"activated_at": "2023-11-07T05:31:56Z",
"activated_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"expires_at": "2023-11-07T05:31:56Z",
"deactivated_at": "2023-11-07T05:31:56Z",
"reason": "<string>"
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
SetAgentKillSwitchRequest Object
Description
Turns the agent kill switch on or off.
Setting active to true while it is already on supersedes the current activation
rather than extending it: the old row keeps its own reason and window as part of the
audit trail, and a new one starts.
True denies agent tool calls for this organization; false lifts the denial.
How long the denial lasts, from now. Ignored when active is false. Capped server-side - a kill switch with no end is how an organization discovers months later that its engineers have been quietly working around it.
Why. Shown verbatim to the developer whose tool call is denied, so write it for them rather than for the audit log - it is the only explanation they get.
Response
The state of the kill switch after the change.
AgentKillSwitch Object
Description
The org-level agent kill switch, and the activation that is in force or last was.
active is what matters; the rest is the record of how it got that way. An
activation is in force only while it has not been deactivated AND expires_at is
still in the future, and that is evaluated on every read - so active can be
false on a row that was never explicitly turned off.
Everything below active describes the LATEST activation whether or not it is
still in force, so an admin reading an inactive switch can still see who last
turned it on and when it ended.
Whether agent tool calls are being denied right now. This is the field to read; it already accounts for expiry.
When the latest activation was turned on. Absent if it never has been.
The admin who turned it on. Absent when that user has since been hard-deleted - the activation record outlives them deliberately.
When the latest activation stops denying on its own. In the past for an activation that already ran out.
When the latest activation was turned off before its expiry, either by an admin or by being superseded. Absent when it ended at expires_at, or has not ended.
The admin's stated reason. Not an internal note - it is shown to the developer whose tool call is denied, in their own terminal.
Was this page helpful?