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/launchroute only carriesplugins+message— not secrets. WithoutOH_CALLBACK_URLthe 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 toPOST /api/v1/app-conversationswith asecretsfield.
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:
Try It End-to-End
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 (not a reference to the bundledon_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 likehooks/on_stop.shwon’t resolve.on_stop.shis kept as the readable, locally-testable source of truth; the identical script is embedded inline inhooks.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:Related
- OpenHands Hooks Guide — full hook documentation
- Plugin System — how plugins work
load-plugin— load this plugin (and pass secrets) via the REST APIcommand-blacklist— the PreToolUse example this one is modeled onlaunch-plugin-badge— turn a plugin into a no-code launch linkconversation-tags— attach metadata (like an external URL) to a conversation
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

