Wake your AI agent when something happens.
galvanize watches the real world (new mail, a file landing in a folder, a git commit, a webhook call, an event from a script) and starts a fresh AI agent session with a prompt you wrote the moment one occurs. Event triggers sit directly in the agent's own tool list (native plugin for Hermes, MCP server for Claude Code and Codex), so "wake me when resumes land" wires an actual push trigger. Passwords go to your OS keyring, each trigger starts watching in seconds, and results arrive wherever you asked (Telegram, Discord, or the log).
What your harness doesn't already do
- Claude Code and Codex can't be woken from outside at all. Their hooks run inside a live session. galvanize supplies the missing inbound door: the daemon watches the world and spawns the session when something happens.
- Hermes has inbound webhooks, but only as admin config, never in the agent's tool list. So agents fall back to cron: an hourly poller that spends an LLM run to announce that nothing arrived.
- Watching is free; thinking costs tokens. Source state (IMAP IDLE, folder, relay) lives in a small daemon with reconnect, dedupe, and catch-up. A model runs exactly once per real event, in a fresh one-shot session you scoped.
Install
Works on Windows, macOS, and Linux (Python 3.10+). No cloning needed; one command installs straight from GitHub:
pipx install "git+https://github.com/lychee888/galvanize.git"
# or: uv tool install --from "git+https://github.com/lychee888/galvanize.git" galvanize
# or: pip install git+https://github.com/lychee888/galvanize.git
Then run setup once:
galvanize init
init enables the Hermes webhook platform (backs up your config first), installs the agent plugin, registers the daemon to start at login, and auto-registers the MCP tool surface into Claude Code / Codex if it finds them on this machine. Everything is confirmed on screen and reversible; --yes accepts defaults.
The daemon's own runtime
daemon install does not adopt the interpreter you happened to run it
from. It provisions (or repairs) an app-owned venv under
~/.galvanize/runtime/venv, validates it launch-equivalently (spawned
isolated, at the launch working directory, with PYTHONPATH/user-site
scrubbed: imports, package versions, a real OS-keyring set/get/delete
roundtrip, windowless target), and records the result in
~/.galvanize/install.json. Only that recorded, probed identity goes into
the persistence entries (Startup .lnk, watchdog task, systemd unit,
LaunchAgent). Falling back to your invoker's interpreter requires passing
the same probe, and install says so out loud; a failed probe changes
nothing on disk. galvanize probe prints the manifest for this
interpreter; galvanize doctor diffs install.json vs the persistence
target vs the live daemon three ways.
Testing/automation seams (env vars): GALVANIZE_RUNTIME_VENV (location),
GALVANIZE_RUNTIME_DISABLE=1 (skip the app venv entirely),
GALVANIZE_RUNTIME_SKIP_PROVISION=1 (validate, never pip),
GALVANIZE_INSTALL_INTERP=<exe> (override the fallback candidate).
After that, the whole surface is five verbs:
galvanize add folder ~/watch --wake hermes # + a live test fire
galvanize add imap you@gmail.com --wake hermes # app-password to keyring
galvanize status # watching? last fire, errors, health
galvanize doctor # deep health check
galvanize test cad-drops # inject a synthetic event through the real path
Why
Polling is what agents reach for because it's the only event surface in their tool list; everything above makes the push version the default.
- Fresh sessions. Each event spawns a clean one-shot run; results are delivered where you asked.
- Management everywhere cron is managed. Dashboard
/triggerstab,/triggersslash command,hermes triggersCLI, agent tools, plusgalvanize status/doctor/daemon: all surfaces, one ops core. - Push email. IMAP IDLE with 25-min re-arm, UID dedupe, reconnect catch-up, keyring-held credentials, and a
migrate hermes-croncommand that converts your existing email pollers. - Zero-inbound-port webhooks. Optional user-owned Cloudflare Worker relay (
relay/worker.js): services POST to your URL, your laptop pulls the queue.
Working with each harness
Hermes
galvanize init does the setup: enables platforms.webhook in your config.yaml (backup saved first), pip-installs the package into the interpreter Hermes runs in, copies the plugin into ~/.hermes/plugins and enables it. Restart the Hermes gateway once to load the webhook platform; the trigger_* tools appear in new sessions. From then on, "wake me when a file lands in ~/cad-drops" creates a real trigger in conversation.
Claude Code / Codex
init writes the MCP server entry into ~/.claude.json / ~/.codex/config.toml automatically when it finds them (Codex gets default_tools_approval_mode = "approve" pre-declared, as newer Codex builds otherwise hide the tools). New session: trigger_add and friends are in the tool list. Wake presets:
galvanize add folder ~/inbox --wake claude # claude -p "{prompt}"
galvanize add folder ~/inbox --wake codex # codex exec (sandbox pre-declared)
galvanize add git-hook ~/code/myrepo --wake codex # wake Codex on commits
Any other CLI agent
galvanize add ... --wake dsh defaults to dsh --profile headless "{prompt}".
The DSH plugin installer saves a custom --wake-profile into both its runtime
profiles and the core's dsh_wake_profile setting, so future CLI and plugin
triggers agree. The CLI also accepts --wake-profile <name> for one trigger.
Existing triggers retain their saved commands. MCP trigger_add requires an
explicit wake target and accepts command, workdir, wake_profile,
relay_url, and relay_token; it does not silently choose Hermes.
galvanize add folder ~/watch --wake shell --command 'myagent run "{prompt}"'
Commands containing {prompt} or {payload} run as an executable plus literal arguments. Event text is never interpolated into a shell command. Use a script for pipelines or redirection, and read GALVANIZE_PROMPT / GALVANIZE_PAYLOAD inside it. On Windows, npm command shims are resolved to their Node entry point; other batch files need an explicit executable or interpreter.
Shell wakes run invisibly on Windows: every child is spawned through one
launch primitive (galvanize/winlaunch.py) with a hidden console inside a
kill-on-close Job Object, so no window ever appears and a timeout or a
surviving descendant takes the whole process tree down with it. The honest
limit: invisibility covers CONSOLE windows only — a shell wake executes
user-declared commands, and any GUI descendant (an editor, a message box,
an Electron app) can still draw its own window.
Relay webhooks
DSH and other shell wakes require a relay for webhooks. Deploy relay/worker.js with a RELAY KV binding and two distinct secrets: RELAY_TOKEN for the daemon to read events, and INGEST_TOKEN for senders to submit events. Set both using wrangler secret put before deploying.
galvanize add webhook --name incoming --wake dsh \
--relay https://your-relay.workers.dev --relay-token "$RELAY_TOKEN"
curl -X POST https://your-relay.workers.dev/ingest/incoming \
-H "Authorization: Bearer $INGEST_TOKEN" \
-H 'Content-Type: application/json' -d '{"message":"hello"}'
The supplied worker requires the ingestion bearer header. Providers that cannot send it need an adapter that validates their native signature before forwarding; pointing their unauthenticated webhook directly at the worker will return 401. Existing deployments must configure INGEST_TOKEN and update senders when installing this worker revision.
Relay receipt and delivery are persisted separately in per-URL SQLite inboxes
under GALVANIZE_HOME (default ~/.galvanize). A page and its receipt cursor
commit together before dispatch. The delivery acknowledgement advances only
past successful or explicitly quarantined events. A failed wake never advances
that acknowledgement; later received events can still run. Normal restarts
resume pending work and do not replay acknowledged IDs, including retained
history returned again by the relay.
Delivery is at least once, not exactly once: a crash after a wake's external
side effect but before the acknowledgement commit can repeat it. Every payload
includes the stable relay event_id; use that ID for idempotent side effects.
Keep the inbox files when restarting. Deleting them intentionally resets replay
protection. IDs are retained without automatic pruning; monitor disk usage for
long-running, high-volume deployments. Events not received before the Worker's
retention period expires cannot be recovered locally. Cloudflare KV is eventually
consistent; this change does not make its timestamp cursor a transactional queue.
Failed dispatches retry up to five times with bounded exponential delay (2, 4,
8, 16 seconds before exhaustion). galvanize status and trigger_status expose
event ID, attempts, and error. After fixing the cause, use
galvanize relay-retry <event-id> to grant another five attempts. Cooldown and
in-flight deduplication deferrals remain pending and are checked again after
two seconds without spending a failed-dispatch attempt. Malformed records are
quarantined individually and remain visible. The Worker wraps
non-object JSON as {"value": ...} and non-JSON text as {"raw": ...}; the
reader also normalizes retained records from older Workers.
watching requires a fresh, connected watcher and a live daemon for folder,
IMAP, and relay sources. Direct Hermes webhooks use gateway health instead;
manual emit triggers have no watcher. IMAP exits IDLE with DONE before searching
or fetching mail, then re-enters IDLE after draining.
Daily use
The agent's own trigger_add tool is the primary creation path; the CLI is the fallback for use without an agent. Every editor — agent tool, CLI, dashboard, galvanize serve — writes through the same transactional SQLite authority (~/.galvanize/galvanize.db) with config revisions, so concurrent edits cannot silently clobber each other. ~/.galvanize/triggers.yaml is an import/export mirror (and the first-seed source): hand-edit it, then galvanize config import triggers.yaml to activate, or config export to start fresh from the live set.
Wake sessions & tool access
A galvanize wake is an event-based cron session: the woken Hermes session
runs with the toolsets named by its webhook route. Since v0.2.4 every
managed route carries an explicit toolsets key defaulting to main-session
parity: the gateway's default distribution ("all available tools, all the
time"), i.e. browser, file, image_gen, terminal, vision, web. Without the
key the gateway falls back to the restricted hermes-webhook composite
(web_search, web_extract, vision_analyze, clarify), which silently cripples
any prompt containing real steps.
Narrow the grant per trigger (CLI; the plugin/MCP trigger_add tool takes
the same list as wake_tools):
galvanize add folder ~/inbox --wake-tools terminal,file # exactly these
galvanize add imap me@x.com --wake-tools none # alert-only posture
--wake-tools none is the security knob for high-volume or untrusted feeds:
inbound content is attacker-influable, and a wake session with autonomous
shell access turns a prompt injection into code execution. Choose it when the
trigger only needs to notify. The explicit choice is recorded on the trigger,
so re-adding or updating it never silently widens or revokes the grant.
Valid names are the Hermes toolset registry keys (checked against the
gateway's allow-list for the webhook platform as of this release):
bot_room, browser, catalog, clarify, code_execution, coding, computer_use, connections, context_engine, cronjob, debugging, delegation, desktop_ui, feishu_doc, feishu_drive, file, image_gen, kanban, memory, onboarding, project, safe, search, session_search, skills, terminal, todo, tts, video, video_gen, vision, web, x_search, yuanbao (plus composites like
hermes-webhook). The gateway re-validates names at spawn and drops unknown
ones, so a stale entry degrades to a no-op.
galvanize doctor prints one wake tools '<name>' row per hermes trigger
(parity / override list / restricted) and ⚠-warns on key-less routes left by
pre-0.2.4 installs. Fix them (recorded choices and manual grants survive):
galvanize doctor --fix # also runs automatically on `galvanize daemon install`
Importing mailbox passwords from himalaya
If your mail already works via himalaya, don't type the app-password a second time:
galvanize secrets import-himalaya --dry-run # list candidates; writes nothing
galvanize secrets import-himalaya # store plaintext IMAP passwords in your OS keyring
galvanize secrets import-himalaya --overwrite # replace galvanize secrets that differ
Passwords land under the same imap:<user> keys galvanize add imap writes, so
existing imap triggers pick the values up on their next reconnect. The himalaya
config is read, never written, and password values are never printed. Accounts
whose password comes from a cmd store are reported but skipped. The config is
auto-detected (~/.config/himalaya/config.toml, %APPDATA%\himalaya, $XDG_CONFIG_HOME);
--config PATH overrides, and an empty GALVANIZE_HIMALAYA_CONFIG disables
discovery entirely.
galvanize doctor adds a drift check for each himalaya account: a password that
is missing from (or differs from) the galvanize store surfaces as a ⚠ warning
with the exact import command to run — non-blocking, and never auto-resynced.
Development
git clone https://github.com/lychee888/galvanize && cd galvanize
python -m venv .venv && .venv/bin/pip install -e ".[dev]" # Scripts/ on Windows
.venv/bin/pytest # unit suite; live-lane + docker tests skip cleanly
The live Hermes-lane test runs inside a Hermes checkout's venv (pytest tests/test_live_hermes_lane.py); the IMAP suite drives a GreenMail container and skips when Docker is unavailable. test_imap_protocol.py always exercises the real IMAP client over TCP against a strict test server that rejects commands during IDLE. Relay Worker tests execute the actual JavaScript with a memory-backed KV fixture, not a Cloudflare deployment.
MIT licensed. Built for the Hermes ecosystem; architecture is harness-neutral.
Metadata
Release files for galvanize 0.2.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| galvanize-0.2.6.tar.gz | 2.5 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| galvanize-0.2.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.6 MB
Release files / galvanize-0.2.6.tar.gz
| Download URL | galvanize-0.2.6.tar.gz |
|---|---|
| Size | 2.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60453bdfa04c6dd227b42c36d665db16aa568c1cade5fad05455352806ff19c4
|
|
BLAKE2b-256 checksum How to use checksums |
597b8022867c61937a092959f44cfbd21d7fbfa82a5c37957c8cba68c5bb0541
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / galvanize-0.2.6-py3-none-any.whl
| Download URL | galvanize-0.2.6-py3-none-any.whl |
|---|---|
| Size | 150.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0021dbd67bfdb81def53d4362e147a826911f76daf9598f69c253ad0a4c83044
|
|
BLAKE2b-256 checksum How to use checksums |
a3d8dfc2c095f8eed9bcd4ee6f226adf079e7769df92df5067503e4dca6d5487
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for galvanize 0.2.6
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.