Panellicense

WHM Standardized Hooks: automate cPanel events with scripts

WHM Standardized Hooks run a script of yours before or after almost any cPanel event — account creation, password change, SSL install — and let pre-hooks block actions outright.

9 min readUpdated 2026-05-17cpanel · whm · hooks · automation
schema: HowToschema: FAQPageschema: BreadcrumbList

Every cPanel server already fires a structured event for the things you wish you could notify on — account created, account suspended, password changed, SSL installed, domain parked. The Standardized Hooks system is how you wire your own script into those events without patching cPanel itself. Hooks survive upgrades, they are scoped by stage (pre or post), and pre-hooks can refuse an action outright by returning a non-zero result.

This guide covers when to use a hook instead of a WHM API token polling loop, how to register one with manage_hooks, how to write the script so cPanel actually invokes it, and how to debug the inevitable "the hook is registered but nothing fires" problem.

When to use a hook (and when not to)

Hooks are the right tool when you need to react synchronously to a cPanel event:

  • Push every new account into your CRM the moment it provisions.
  • Sync user passwords to a downstream system (LDAP, Mattermost) without scraping logs.
  • Block the creation of accounts on domains that match a regex (an industry filter, a legal hold list).
  • Tear down billing-side records when an account is removed in WHM before the billing poller next runs.

They are the wrong tool when you want a scheduled summary, a daily reconciliation, or anything that can tolerate a 5-minute lag. For those, poll a scoped WHM API token on a cron — a hook that fires 4,000 times a day per server because someone is iterating accounts in a script is a load problem you do not need.

The other constraint: hooks run as root (or as the user that owns the event, for some user-context hooks). Code that runs as root inside the cPanel event path is a supply-chain target. Audit anything you put there as if it were a CI runner with prod access.

Anatomy of a hook

A registered hook is a row in /var/cpanel/hooks.yaml that points cPanel at:

  • A categoryAccounts, Whostmgr, Cpanel, Backup, Mail, etc.
  • An event within that category — Create, Remove, Modify, Suspend, SiteIP, Changepasswd.
  • A stagepre (runs before the event, can block it) or post (runs after, cannot block but sees the result).
  • A script or Perl module to invoke. Script hooks are simpler; module hooks live in /var/cpanel/perl/ and are appropriate when you need to share state across multiple events.

When the event fires, cPanel passes a JSON payload on stdin. Your script reads it, optionally does work, and prints a JSON response on stdout: {"result": 1} for "carry on" or {"result": 0, "msg": "human-readable reason"} to block a pre-event.

Step 1 — Discover the event you actually want

cPanel's event names are not always intuitive — Whostmgr::Accounts::Create and Cpanel::Accounts::Create are different events with different payloads. Before you register, ask the server what's available:

/usr/local/cpanel/bin/manage_hooks list

That dumps registered hooks. To see the event catalog and the data each one passes, inspect the hookable function map:

ls /usr/local/cpanel/Cpanel/Hooks/
ls /var/cpanel/perl/Cpanel/Hooks/

For account events specifically, the canonical reference is /usr/local/cpanel/Whostmgr/Accounts/Create.pm — read the source for the exact field names you'll see in the payload. There is no substitute for this when the docs lag a release.

For a quick payload sample, register a logging stub against the event, fire the event once, and grep the log:

cat > /usr/local/bin/hook-debug.sh <<'EOF'
#!/bin/bash
cat >> /var/log/hook-debug.log
echo '{"result":1}'
EOF
chmod +x /usr/local/bin/hook-debug.sh

/usr/local/cpanel/bin/manage_hooks add script /usr/local/bin/hook-debug.sh \
  --category Whostmgr --event Accounts::Create --stage post

Create a test account in WHM, then cat /var/log/hook-debug.log — every field you can read from inside a hook is in that file.

Step 2 — Write the script

Hook scripts must be executable, must read stdin, and must print exactly one JSON object on stdout. Anything else (a stray print from a debug line, a Python framework writing a banner) confuses the parser and the hook is treated as a soft failure.

A minimal account-creation post-hook in Python:

#!/usr/bin/env python3
import json
import sys
import urllib.request

payload = json.loads(sys.stdin.read())
data = payload.get("data", {})

# Whostmgr::Accounts::Create post-hook payload contains the new account context
account = {
    "username": data.get("user"),
    "domain": data.get("domain"),
    "plan": data.get("plan"),
    "ip": data.get("ip"),
}

req = urllib.request.Request(
    "https://crm.internal.example.com/hooks/cpanel-account",
    data=json.dumps(account).encode("utf-8"),
    headers={"Content-Type": "application/json",
             "Authorization": "Bearer REPLACE_WITH_SECRET"},
    method="POST",
)
try:
    urllib.request.urlopen(req, timeout=5)
except Exception:
    # Post-hooks cannot undo the account creation — never raise here
    pass

print(json.dumps({"result": 1}))

Two non-obvious rules:

  • Post-hooks should never raise. The account has already been created. An exception in your hook does not roll it back; it just generates noise in error_log and leaves your downstream out of sync. Wrap network calls in try/except and queue retries in your own system.
  • Time out fast. cPanel waits for your hook to complete before continuing. A hook that blocks for 30 seconds on a flaky CRM hold WHM up for 30 seconds for every account created. Set a tight timeout and fail open.

For a pre-hook that blocks, the script returns result: 0:

print(json.dumps({
    "result": 0,
    "msg": "Domain matches the reserved-name list; create disallowed."
}))

WHM surfaces msg to the operator. Keep it specific — "blocked by policy" wastes a support ticket.

Step 3 — Register the hook

manage_hooks add takes a script or module argument plus the category, event, and stage. Use the full absolute path; hooks do not inherit a shell PATH:

/usr/local/cpanel/bin/manage_hooks add script /usr/local/bin/sync-account-to-crm.py \
  --category Whostmgr --event Accounts::Create --stage post

Verify it's wired:

/usr/local/cpanel/bin/manage_hooks list category=Whostmgr event=Accounts::Create

You should see the script path, the stage, and the hook ID. To remove:

/usr/local/cpanel/bin/manage_hooks delete hook_id=<id>

The hook ID is what list printed in the first column.

Step 4 — Debug a hook that "should be firing"

Three failure modes account for most of the silent-hook tickets:

  • Script is not executable. chmod +x it. manage_hooks add will register a non-executable file without complaining.
  • Script writes to stdout before the JSON response. Any line of debug output gets parsed as JSON and your hook is treated as malformed. Send logs to stderr or to a file instead.
  • You registered the wrong category. Whostmgr::Accounts::Create fires when an account is created from WHM or createacct. Cpanel::Accounts::Create does not exist — there is no per-user "create my own account" surface — but Cpanel::Accounts::SiteIP does fire on the user side, for example. Read the source under /usr/local/cpanel/Whostmgr/ or /usr/local/cpanel/Cpanel/ to confirm the namespace.

cPanel logs hook execution to /usr/local/cpanel/logs/error_log and, for blocked pre-hooks, to the originating WHM action's response. Tail both while you reproduce:

tail -f /usr/local/cpanel/logs/error_log /var/log/hook-debug.log

If nothing appears in error_log when you fire the event, the hook is not registered against the right event. If error_log shows the hook firing but no log file appears, the script is being invoked but exiting before it writes — usually a permission problem on the log path.

Step 5 — Test the pre-hook block path before relying on it

A pre-hook that blocks is a policy enforcement point. You want to know it actually blocks, not just that it runs. Register the hook, then attempt the action that should be refused. For an account-creation block:

whmapi1 createacct username=blocked01 domain=test-reserved.example.com plan=default

If the hook is doing its job, the response includes result: 0 and your msg. If the account creates anyway, the hook is registered against the wrong stage (you registered post, not pre) or the script is returning result: 1 for the input you thought was blocked.

Pre-hooks that block accidentally are worse than no pre-hook. Always include a kill switch — for example, an environment variable check at the top of the script — so you can disable the policy without unregistering the hook:

import os
if os.environ.get("CPANEL_HOOK_BYPASS") == "1":
    print('{"result":1}')
    sys.exit(0)

CPANEL_HOOK_BYPASS then becomes an emergency override. Set it on the WHM process environment only when you genuinely need to bypass.

Next steps

Do WHM hooks run on every cPanel server in a DNS cluster?+
Only on the server where the event fires. A DNS cluster replicates zone data, not hook payloads. If you need cluster-wide behaviour — say, syncing accounts to a remote system on creation — register the hook on every WHM node, ideally via configuration management.
Can a pre-hook modify the event payload before WHM acts on it?+
No. Pre-hooks see the payload and decide pass or block. They cannot rewrite the username, change the package, or alter the IP. If you need to mutate, use a post-hook that calls a WHM API to modify the resulting account, accepting the brief window where it exists in the original state.
How do I run different hooks per package or per reseller?+
Hooks are global; cPanel does not scope by package. Do the filtering inside your script — read the package or reseller from the event payload and exit with result: 1 if it doesn't match. Splitting one script per profile is cleaner than registering multiple hooks against the same event.
Will Standardized Hooks slow down account creation under load?+
Yes, linearly. WHM blocks on each registered hook before continuing. A 100ms hook adds 100ms per account event. Two hooks add 200ms. For high-throughput provisioning, keep hook scripts under 50ms or queue the slow work to a background process and return immediately.
Are hooks logged for audit purposes?+
Hook execution is logged to /usr/local/cpanel/logs/error_log with the hook ID and exit status. The hook script itself is responsible for logging what action it took. For compliance traceability, write a structured log line per invocation including the event payload's key fields and your hook's decision.
Can I write hooks in languages other than Perl?+
Yes. Script hooks invoke any executable — Bash, Python, Go binary, anything that reads stdin and prints a JSON response on stdout. Module hooks must be Perl because cPanel loads them into its own interpreter. For most automation needs, a script hook in the language your team already maintains is the right call.
Switch in an afternoon

Switch from your current reseller — free.

We migrate active cPanel, Plesk, LiteSpeed and CloudLinux licenses from any reseller. We prorate the first month so you never pay twice, and your customers see zero downtime during the swap.