View source on GitHub
POSTs to a URL you
control, optionally attaching a payload file’s contents.
This is the “push instead of poll” pattern: keep your existing polling loop as a
safety net, but let the callback wake you up immediately in the common case.
About the badge: clicking it loads the plugin into a fresh conversation,
but the
/launch route only carries plugins + message — not secrets.
Without OH_CALLBACK_URL the hook is a deliberate no-op, so the badge is a
“see the plugin load” demo. To actually receive a callback you must supply the
secrets, which means the API path below (load_finish_callback.py) or your own
call to POST /api/v1/app-conversations with a secrets field.No customer information lives in this plugin. The callback URL, an optional
shared-secret token, and an optional payload file path all come from
conversation secrets at start time. The repo ships only a local test
receiver so you can prove the end-to-end flow against your own machine.
What’s in the Box
How It Works
Configuration
Everything is supplied as conversation secrets — nothing is hard-coded:
Default body when no payload file is given:
Run It
You need two things reachable from the sandbox: a running receiver and a public URL that forwards to it. The receiver is stdlib-only; the loader needsrequests:
export OH_API_KEY="sk-oh-...").
1. Start the receiver
204.
2. Expose it to the internet
The sandbox runs in the cloud, so it needs a public URL to reach your laptop. Use any tunnel, e.g.:https://… URL it gives you. That’s your OH_CALLBACK_URL.
3. Start a conversation with the plugin loaded
Use the bundled turnkey helper. It loads the plugin and passes the callback settings as conversation secrets, so the Stop hook picks them up as environment variables:--callback-payload "/path/in/sandbox/body.json" (the path is resolved inside
the sandbox, not on your laptop).
Prefer the generic loader? load-plugin does the same
thing with --secret flags:
Heads-up: the callback fires on every transition to
FINISHED — so it
also covers follow-up messages you send later, not just the first run.Verify the Hook Locally (No Sandbox Needed)
You can exercise the exact hook script against a local receiver in one shell:The Hook
The magic is inhooks/hooks.json:
Stop— runs when the agent tries to finish (the terminal-state moment).matcher: "*"— Stop hooks aren’t tool-specific, so match everything.type: "command"— thecommandis a POSIX-sh script run via/bin/sh -c.async: true— fire-and-forget, so the callback never delays finishing.- Exit codes:
0= allow the agent to finish (this hook always does);2would block finishing (we deliberately never do that).
curls
your URL with a short timeout, swallowing errors.
Why inline?
Why inline?
Why inline (not a reference to the bundled
on_stop.sh)? When hooks run as
a plugin, they execute with the working directory set to the agent’s
workspace (not the plugin directory), and there is no plugin-root path
variable — so a relative path like hooks/on_stop.sh won’t resolve.
on_stop.sh is kept as the readable,
locally-testable source of truth; the identical script is embedded inline
in hooks.json, which is the copy that actually runs. If you edit the script,
re-embed it:Reliability: Callback + Polling
The callback is a latency optimization, not a delivery guarantee. It won’t fire if:- the sandbox dies or the run errors out before reaching
FINISHED, - the receiver is down or the URL is unreachable, or
- the POST times out.
Hook Types
Hooks can intercept different lifecycle events:Real-World Use Cases
- Windmill / workflow engines — get pinged when a run finishes instead of polling every few seconds
- CI pipelines — kick off the next stage the moment the agent is done
- Dashboards / queues — mark a job complete in real time
- Chat notifications — post “run finished” to Slack/Teams from your own backend
Related
OpenHands Hooks Guide
full hook documentation
Plugin System
how plugins work
load-plugin
load this plugin (and pass secrets) via the REST API
command-blacklist
the PreToolUse example this one is modeled on
launch-plugin-badge
turn a plugin into a no-code launch link
conversation-tags
attach metadata (like an external URL) to a conversation

