Operation Hooks ​
Operation Hooks runs a script of yours on the server before or after a panel operation: creating or deleting a hosting account or a domain, changing a DNS record, or creating or deleting a mailbox. A hook that runs before an operation can refuse it, so you can enforce your own rules, and a hook that runs afterwards can tell another system what changed.
Available since panel 1.9.0.
Overview ​
URL: /admin/settings/operation-hooks
Open it from Settings > System > Operation Hooks. Only administrators can open it, because a hook script runs as root on the server. Resellers and clients do not see the page, and their operations run the hooks like any other.
The list shows each hook's Name, Event, Phase, Hook script, whether it is Enabled, and its Last run with the Result, Exit code, and Duration. Add hook creates one.
Events and phases ​
| Event | Before the operation | After the operation |
|---|---|---|
| Hosting account created | yes | yes |
| Hosting account deleted | yes | yes |
| Domain created (a domain, subdomain, or parked domain) | yes | yes |
| Domain deleted | yes | yes |
| DNS record created | no | yes |
| DNS record updated | no | yes |
| DNS record deleted | no | yes |
| Email account created | no | yes |
| Email account deleted | no | yes |
Before the operation (can abort it). The hook runs before the panel changes anything on the server. Exit code 0 lets the operation continue. Any other exit code, a hook that runs past its timeout, or a script that cannot be started refuses the operation. The person who started it sees "The operation was refused by the pre-hook" with the hook's name and the first line the script printed. A refused operation leaves nothing half created. When several hooks apply, they run in the order they were created.
After the operation (informational). The hook runs in the background shortly after the operation has finished. It cannot stop or fail the operation: a non-zero exit code, a timeout, or a start failure is recorded on the hook and nothing else happens.
DNS record and mailbox events have no before phase. The form offers only the phases an event supports.
Preparing a script ​
A hook runs a script you place on the server. The script must be:
- an executable file under
/usr/local/bolt/hooks, - owned by
root, - writable by nobody but
root. A script that is group or world writable is refused, both in the list of scripts and when it runs.
The script runs as root, with the hooks directory as its working directory. It receives the operation as one JSON document on standard input. Nothing from the payload is put on the command line.
These environment variables are set for the script:
| Variable | Contents |
|---|---|
BOLT_HOOK_EVENT | The event, for example domain.create. |
BOLT_HOOK_PHASE | pre or post. |
BOLT_HOOK_ID | The hook's ID in the panel. |
BOLT_HOOK_SCRIPT | The script being run. |
What the script prints is captured and shown with the run.
Adding a hook ​
- Click Add hook.
- Give the hook a Name, and optionally a Description.
- Choose the Event and the Phase.
- Pick the Hook script from the scripts the server finds in the hooks directory, or use Enter a path to type one.
- Set Timeout (seconds), between 5 and 300. The script is stopped when it runs longer. A before hook that times out refuses the operation.
- Save.
A hook can be switched off with its Enabled toggle, in the form or in the list, without deleting it.
Testing a hook ​
Run test runs the hook now with a sample payload. The payload carries "test": true and has no real subject, so a script can recognise a test and skip its real work. The test run is recorded like any other run.
Runs ​
The hook's edit page lists its Runs: when each one ran, the result, the exit code, the duration, the Captured output, and the Payload passed to the script. Use it to see why a hook refused an operation or why a background hook failed.
Creating, changing, and deleting a hook is recorded in the Activity Log.
The payload ​
{
"version": 1,
"event": "domain.create",
"phase": "pre",
"occurred_at": "2026-09-07T18:12:04+00:00",
"server": { "hostname": "srv1.example.com", "panel_version": "1.9.0" },
"actor": { "type": "admin", "id": 1, "name": "admin" },
"subject": { "...": "see below" }
}actor.type is admin, reseller, hosting_account, customer, or system (the scheduler, the installer, or an API call without a signed-in user).
The subject depends on the event:
| Subject | Fields |
|---|---|
| Hosting account | type, id, username, domain, hosting_plan (id, name), reseller_id, linux_uid, status. Before the account is created, id, linux_uid, and status are null. |
| Domain | type, id, domain, domain_type (domain, subdomain, or parked), parent_domain_id, document_root, hosting_account (id, username). Before the domain is created, id and document_root are null. |
| DNS record | type, id, zone, name, record_type, content, ttl, priority. |
| Mailbox | type, id, address, domain, hosting_account_id. |
Exit codes ​
| Outcome | Before the operation | After the operation |
|---|---|---|
0 | The operation continues. | Recorded as a success. |
| Any other code | The operation is refused; the code and the first output line are shown. | Recorded as failed. |
| Timeout | The operation is refused. | Recorded as a timeout. |
| Could not start | The operation is refused. | Recorded as an error. |
Example ​
A before hook on Domain created that refuses names under one domain:
#!/bin/sh
# /usr/local/bolt/hooks/refuse-reserved-names.sh
payload=$(cat)
domain=$(printf '%s' "$payload" | jq -r '.subject.domain')
case "$domain" in
*.internal.example.com)
echo "Domains under internal.example.com are managed elsewhere."
exit 1
;;
esac
exit 0The example reads the payload with jq. Install it on the server if your script uses it.
Related pages ​
- Domain Security - the built-in list of domains nobody can add, which needs no script.
- Activity Log - the record of changes to hooks.
- Hosting Accounts - the operations the account events follow.