Skip to content

Docs Hub

Hooks

A hook is a webhook URL for one of your scripts. When anything posts to it, such as a CI job on each push, the script runs on your Mac. It runs where you choose: in a fresh clone of a VM, in the VM itself, or on the Mac. A fresh clone is a throwaway copy of the VM, like a fresh CI runner.

Each hook belongs to the Mac that made it. Only that Mac runs it, and only while VMPal is open there.

Ask your AI agent

The easiest way to make a hook is to ask your coding agent. It prepares the VM, writes the script and tries it. Then it makes the hook and adds it to your CI. When a run fails, it reads the logs and tells you why. It uses VMPal's MCP tools, or vmpal script and vmpal hook in its terminal. Both do what VMPal's Hub does. VMPal also gives your agent the full guide, vmpal help hook.

  1. In VMPal, open Settings › AI Agents and turn on Let coding agents on this Mac manage all VMs.
  2. Click Add to Claude Code, or another agent's button (which ones), and restart the agent.
  3. Ask it, for example:
    • "Create a VMPal hook that tests this app in my Ubuntu VM whenever I push to main."
    • "Add a GitHub Actions step that posts to my VMPal hook and fails when the tests fail."
    • "Run my test script now and tell me why it failed."
    • "Rotate my hook's URL and update the GitHub secret."

vmpal is VMPal's command-line tool, like docker or gh. To run the commands yourself, install it.

The rest of this page is how hooks work, for when you want to know.

What you need

  • A VMPal license. Hooks need one, but scripts and Run Now don't. Activate it in VMPal › Settings › License.
  • VMPal open, and the Mac awake. Hooks run nothing while VMPal is closed or the Mac sleeps.
    • To catch every push, have VMPal open when you log in. Add it in System Settings › General › Login Items & Extensions. Or turn on Start when you log in for one of your VMs. That opens VMPal in the background when you log in.
    • Keep the Mac from sleeping in System Settings › Energy (on a laptop, Battery › Options…).
    • If vmpal opened VMPal in the background, hooks wait until you open VMPal yourself, or run vmpal hook create.
  • A script that works where it runs. For example, a fresh clone of a VM needs git and your build tools. For a private repository, it also needs a read-only deploy key. Scripts says how, and vmpal help hook has the commands.

Make a hook

You make hooks in VMPal, not on vmpal.com.

  1. In VMPal's Hub, right-click the script and choose Create Hook….
  2. Choose where it runs, and click Create Hook.
  3. The sidebar shows the hook's URL as https://vmpal.com/v1/hooks/••••••••. Click it to copy it.

Copy Hook, the button beside the URL, also copies it as a ready-to-use snippet: a cURL command, a GitHub Actions step, JavaScript or Python. VMPal keeps the URL in your login keychain. It never writes the URL in its files or logs.

With vmpal

This example tests an app from GitHub in a fresh clone of your Ubuntu VM:

cat > test-app.sh <<'EOF'
#!/bin/bash
set -euo pipefail
# The message is JSON, {"sha": "<commit>"}: take the commit alone, 7 to 40 hex digits, never anything else.
sha=$(printf '%s' "${VMPAL_MESSAGE_BODY:-}" | sed -n 's/.*"sha" *: *"\([0-9a-f]\{7,40\}\)".*/\1/p')
rm -rf app
git clone --quiet https://github.com/you/app.git app
cd app
git checkout --quiet "${sha:-main}"
make test
EOF
vmpal script create "Test App" --file test-app.sh --run-in "Ubuntu"
vmpal script run "Test App" --wait
vmpal hook create "Test App"
  • vmpal script create saves the script in VMPal's Hub. --run-in "Ubuntu" runs it in a fresh clone of the Ubuntu VM.
  • vmpal script run runs the script now, like a hook's run, but without a message. It doesn't count toward the hook's 10 messages an hour.
  • vmpal hook create makes the hook and prints its URL. vmpal hook url "Test App" prints it again.
  • vmpal script edit changes the script and where it runs. The hook keeps its URL.

Anyone with the URL can post anything. That's why the script takes only a commit from the message, and nothing else. Without a commit, it tests main. rm -rf app gives each run a clean start when the script runs in the VM itself. For a private repository, clone [email protected]:you/app.git instead.

The message's variables

A hook's run gets the message as environment variables:

Variable Value
VMPAL_MESSAGE_BODY By default, the first 32,000 bytes of what was posted
VMPAL_MESSAGE_TITLE By default, the hook's name, which is the script's name
VMPAL_MESSAGE_ID The message's ID, the same one the post's answer gives
VMPAL_MESSAGE_TIME When vmpal.com got the message, in ISO 8601
VMPAL_HOOK_ID The hook's ID

The run also gets the script's own variables. In PowerShell, read them as $env:VMPAL_MESSAGE_BODY and so on. Runs from Run Now or vmpal script run have no message, so they don't get these.

To make the title and body from other parts of the post, use the hook's Message tab in the License Manager.

Send a message

To run the hook, post to its URL, like any webhook. The examples on this page keep the URL in VMPAL_HOOK_URL. In CI, make it a secret. On your Mac, set it with VMPAL_HOOK_URL=$(vmpal hook url "Test App").

curl -fsS "$VMPAL_HOOK_URL" -H 'Content-Type: application/json' --data-binary '{"sha": "'"$(git rev-parse origin/main)"'"}'

Send a commit that's already on GitHub. The run gets your code from there. vmpal.com answers 202 with {"message_id":123}.

  • The body can be text, JSON, XML or a form, up to 64 KiB.
  • An Idempotency-Key header makes retries safe. A retry with the same key and body gets the same message back, with "duplicate": true. It runs only once. The same key with another body gets 409.
  • 429 with Retry-After means your hooks are over their posting limit. Wait that many seconds, then send it again. vmpal.com doesn't keep the post for later.

Answer codes lists every answer.

Follow it

Each message has its own status URL: the hook's URL, then /messages/ and the message's ID. Read it to see how the runs went.

curl -fsS "$VMPAL_HOOK_URL/messages/123"
{
  "message_id": 123,
  "created_at": "2026-10-13T09:30:00Z",
  "runs": [
    {
      "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
      "target": "Ubuntu",
      "state": "failed",
      "exit_code": 2,
      "detail": "It exited with status 2.",
      "started_at": "2026-10-13T09:30:01Z",
      "ended_at": "2026-10-13T09:34:12Z",
      "updated_at": "2026-10-13T09:34:12Z"
    }
  ],
  "done": true
}
Field What it says
state waiting, starting or running while the run is in progress. Then passed, failed, timedOut, stopped, interrupted or skipped
exit_code The script's exit code. As in any CI, 0 passes
detail Why a run failed, waits or was skipped. It's in the language VMPal uses on that Mac, so check state and exit_code, not detail
done true once there's at least one run, and every run has ended

The answer never has a run's output, because a log can hold anything the script printed. History and logs says where to read it.

runs stays empty until VMPal takes the message. That happens within 30 minutes of the post, or never. A message with no runs after that never runs. The reason is one of these:

  • VMPal was closed, or the Mac was asleep.
  • The hook was paused. vmpal hook url says why.
  • The script runs nowhere. vmpal script show says where it runs.
  • A newer message came first.

vmpal hook messages "Test App" lists the hook's last messages, each with the runs it started.

To get each change as it happens, ask for server-sent events with -N -H 'Accept: text/event-stream'. The same URL then sends a run event whenever a run changes, and done at the end:

curl -fsS -N -H 'Accept: text/event-stream' "$VMPAL_HOOK_URL/messages/123"

A new message during a run

When a new message comes while a run is in progress, it waits for that run, or stops it. Choose which in the script's editor, under New Message:

New Message What happens
Queue After the Current Run (--new-message queue, the default) The current run finishes, then the new message runs
Cancel the Current Run and Run It (--new-message cancel) The new message stops the current run, then runs

Only the newest message waits. A newer one, or Run Now, takes its place. Run Now never stops a run.

In CI

This GitHub Actions step posts a message, waits for its runs, and fails when a run fails. Use it on a Linux or macOS runner. First, save the hook's URL as the repository secret VMPAL_HOOK_URL. In GitHub, that's Settings › Secrets and variables › Actions.

- name: Test in VMPal
  env:
    VMPAL_HOOK_URL: ${{ secrets.VMPAL_HOOK_URL }}
  run: |
    message=$(curl -fsS --retry 5 "$VMPAL_HOOK_URL" -H 'Content-Type: application/json' \
      -H "Idempotency-Key: $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
      --data-binary "{\"sha\": \"$GITHUB_SHA\"}" | jq -r .message_id)
    [ -n "$message" ] && [ "$message" != null ] || exit 1
    for i in $(seq 1440); do    # 2 hours
      sleep 5
      curl -fsS "$VMPAL_HOOK_URL/messages/$message" > runs.json || continue
      jq -e .done runs.json > /dev/null && break
      # No runs 35 minutes on: VMPal passed the message over, or wasn't open.
      [ "$i" -lt 420 ] || jq -e '.runs != []' runs.json > /dev/null || { echo "VMPal didn't take the message." >&2; exit 1; }
    done
    jq -r '.runs[] | "\(.target): \(.state) \(.detail // "")"' runs.json
    jq -e '.done and all(.runs[]; .state == "passed")' runs.json > /dev/null
  • The step waits up to 2 hours. If your script's timeout is longer than an hour, make the loop longer.
  • It fails when VMPal hasn't taken the message 35 minutes after the post.
  • A newer push can take the place of a waiting run, or stop the current run. The older run is then skipped or stopped, and its job fails. To cancel the older job instead, give the workflow concurrency: { group: vmpal, cancel-in-progress: true }.

A ready-made step

VMPal's Hub can write a GitHub Actions step for you, in Python. Choose Copy Hook › Copy Hook as GitHub Actions Step, beside the hook's URL. Or print it with vmpal hook url "Test App" --as github. Use it on a Linux or macOS runner.

  • It reads the URL from the repository secret VMPAL_HOOK_URL.
  • It posts {"ref": …, "sha": …} as JSON. The script above reads that too.
  • It prints each run's state as it changes.
  • It exits with the first failed run's status.
  • It gives up when VMPal hasn't taken the message 35 minutes after the post, or after 6 hours.

--as python prints a program that follows the runs the same way. --as curl and --as javascript only post {}. These three have the URL itself in them, so keep them out of files you commit.

History and logs

  • History, in the script's sidebar in the Hub, lists each message as an item titled Hook. The item shows the start of what was posted, and the runs it started. Runs from Run Now show as Run Now.
  • Double-click an item to see the whole message and each run's log. Logs are on your Mac, and Scripts says where.
  • In Terminal, vmpal script runs "Test App" lists the runs, and vmpal script log "Test App" prints the newest run's output.
  • With the script's Send Output to vmpal.com on, a hook's runs also send their logs to the License Manager.
  • VMPal notifies you how each run went, when a message didn't run, and when a hook pauses.

When a hook pauses

A paused hook runs nothing. Messages that come during a pause never run. The script's row in the Hub says why, and the sidebar says more:

  • Hook paused: no license
  • Hook paused: too many messages. This pause ends by itself after an hour.
  • Hook paused: changed on vmpal.com

Alerts while VMPal is closed

While VMPal is closed, a message makes the hook's Mac show an alert. It's a push notification, like any app's. It shows the hook's name, then "Got a message. Open VMPal to run it." VMPal runs a message only while it's less than 30 minutes old, so open VMPal before then.

  • Alerts need VMPal to be allowed to show notifications. VMPal asks when you make your first hook.
  • The alert never shows the message's text, because anyone with the URL can post anything.
  • If an alert can't reach the Mac within 30 minutes of its message, such as while the Mac sleeps, it never comes. By then, VMPal wouldn't run that message anyway.
  • When VMPal runs a message itself, it hides that message's alert.

In the License Manager

The License Manager is the account page on vmpal.com for VMPal licenses. Your hooks show there under Hooks, for the account that owns your Mac's license. The License Manager can't make a hook or show a hook's URL.

  • Recent runs lists each message's runs. It shows their logs when the script's Send Output to vmpal.com is on.
  • Edit has two tabs, Configuration and Message. The table below says what's on each.
  • Delete deletes a hook. Its script stays in VMPal. Create Hook… in VMPal makes a new hook, with a new URL.
Tab What it has
Configuration The hook's name, category and icon. Data retention, from 1 to 365 days (30 by default). Enable this hook. A hook turned off answers posts with 404
Message How each message's Title and Details are made from parts of the post. The script gets them as VMPAL_MESSAGE_TITLE and VMPAL_MESSAGE_BODY. A post they can't read gets 422, and runs nothing

On a company's license, the company's account is the owner. It sees the hooks its people's Macs made, and reads their output. It can also turn them off or delete them.

Limits and safety

  • The URL is a secret. Anyone who has it can run the script. Keep it in your CI's secrets, not in your code.
  • Rotate a URL that got out. Choose Rotate Hook URL… in VMPal's Hub, or run vmpal hook rotate <script>. The hook gets a new URL, and the old one stops working at once. Posting to it, or following its messages, gets 404. Runs still waiting for its messages are skipped.
  • Delete a hook with Delete Hook…, or vmpal hook delete <script>. Its URL stops working the same way. The script stays, and Run Now still runs it. Delete Script… deletes its hook too. Without a license, though, that hook stays on vmpal.com, running nothing.
  • Never put secrets in a message. vmpal.com keeps messages for 30 days, unless you change the hook's Data retention in the License Manager. When you delete a hook, its messages and their runs' output go 7 days later.

Rotate Hook URL… and Delete Hook… are in the script's menu, and under Copy Hook beside the URL. They need a license.

Limit
Messages At most 10 run in an hour. The next one pauses the hook for an hour
A message's age Only a hook's newest message runs, and only if it's less than 30 minutes old when VMPal sees it. So if VMPal was closed, or the Mac asleep, for longer than that, the message never runs
Runs A run stops at the script's timeout, an hour by default. Runs also follow the script's limits, such as two clones at once
Hooks The Macs on a license key can have 10,000 hooks in all. They can make 10 a minute
Posts 200 a minute for all of a license key's hooks together, with 8 MiB of bodies. Each of your license keys has its own limit. Past it, a post gets 429 with Retry-After, the seconds until the minute ends
Reads A hook's messages take a burst of 20 reads, then 4 a second. Past that, a read gets 429 with Retry-After
Event streams 4 at once for a hook's messages. A stream ends after an hour without done. EventSource then reconnects

Answer codes

What vmpal.com answers when you post to a hook, or follow one of its messages:

Code What it means
202 vmpal.com took the message: {"message_id":123}. A retry with the same Idempotency-Key and body also gets "duplicate": true
404 The URL is wrong or was rotated, or its hook was deleted or turned off
409 The same Idempotency-Key came with another body. Or, with a code, nothing runs this hook any more (below)
413 The body is over 64 KiB
415 The body isn't UTF-8 text, JSON, XML or a form
422 The hook's Message rules can't read the post. Nothing runs
429 Too many posts or reads. Wait as many seconds as Retry-After says, then send it again. Nothing is kept for later

A 409 with a code means nothing runs this hook any more. Its error says why and what to do. Following a message of such a hook gets the same 409, so a CI step stops at once, with the reason.

code What it means
mac_removed The Mac that made the hook was removed from its VMPal license. Activate VMPal on that Mac again, and the same URL works. If error says the Mac was removed for good, make a new hook on another Mac
mac_moved That Mac now uses another person's license. Make a new hook on it
license_inactive The license key that Mac uses isn't active. Activate VMPal on that Mac with another of your license keys, or make a new hook on a Mac with an active license

For the whole walk-through, run vmpal help hook, or ask your AI agent.