The catalog and zerto_check_tool are advice. An MCP server cannot see or
block another server's tool calls, so a model that skips the guard is not
stopped by anything, and the change lands with no checkpoint behind it.
A PreToolUse hook runs in the host, where the tool call actually pauses.
That turns capture-before-execute from a convention into something the
host enforces.
read-only no decision, runs
mutating, checkpoint confirmed no decision, plus additionalContext
telling the model which checkpoint to
recover from
mutating, checkpoint failed DENY, the call never happens
mutating, VM unknown to Zerto prompt, nothing to rewind to
mutating, no VM in the args prompt, the catalog's vm_arg missed
unlisted tool prompt
Decisions go out as JSON rather than exit 2. Exit 2 blocks
unconditionally but discards the JSON, and with it the reason, so the
model would be refused without being told why.
A lookup miss is not a refusal. Asking Zerto for a plain hostname by
vmIdentifier returns HTTP 400, and an early version reported that as
"could not tag a checkpoint", which denied changes to machines Zerto had
simply never heard of. Those are now separated: unknown VM prompts, a VM
Zerto knows but will not tag denies.
Timeouts are budgeted for the slow path. A successful tag took about 4s,
but a refusal took 63s, because wait_for_tag spends 45s waiting for a
checkpoint that will never arrive on an AWS or Azure protected VPG. If
the host's timeout fires first it cancels the hook and discards its
output, and the call proceeds unguarded, so the hook's own budget (150s)
stays under the configured one (180s): better to deny than be cancelled.
Verified against a live ZVM 10.9.10. jp-ubuntu tagged checkpoint 7180 and
was allowed; win2019-1, whose VPG is protected at an AWS site where
tagged checkpoints are unsupported, was denied. Both held under
permission_mode bypassPermissions, which is when an agent is most likely
running unattended.
Broad excepts in hooks/ are deliberate and scoped in pyproject: a hook
that raises breaks the tool call it exists to protect.
pytest 60 passed (12 new).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
PreToolUse guard hook
Makes the rewind guard enforceable instead of advisory.
The MCP server cannot see another server's tool calls, so zerto_check_tool and
zerto_guard_before_mutate only work if the model chooses to call them. A model
that skips the step is not stopped by anything. A PreToolUse hook runs in the
host, where the tool call genuinely pauses, so a failed checkpoint stops the
change.
Decisions
| situation | decision | effect |
|---|---|---|
tool in read_only_tools |
none | runs, no checkpoint |
| mutating, checkpoint confirmed | none, plus additionalContext |
runs, and the model is told which checkpoint to recover from |
| mutating, checkpoint failed | deny |
the call never happens |
| mutating, VM unknown to Zerto | prompt |
human decides; nothing to rewind to |
| mutating, no VM in the arguments | prompt |
catalog's vm_arg did not match |
| unlisted tool | prompt |
nobody said it was read-only |
ZERTO_HOOK_UNKNOWN switches the unlisted case to allow or deny.
Install
cp hooks/settings.example.json /tmp/x # then merge the hooks block into
# .claude/settings.json
export ZERTO_REWIND_CONFIG=/abs/path/config.json
Both paths in the command must be absolute, and the interpreter must be the venv that has this package installed.
Timeouts, which matter here
The hook is synchronous: the host waits. That is the point, because the checkpoint has to exist before the change does.
Budget for the slow path, not the fast one. A successful tag took about 4s
against a healthy VPG, but a refusal took 63s: wait_for_tag waits 45s for
a checkpoint that is never going to appear, which is exactly what happens on a
VPG whose protected site is AWS or Azure, where tagged checkpoints are not
supported.
So:
timeoutin settings.json: 180 (seconds)ZERTO_HOOK_GUARD_TIMEOUT: 150 (seconds), kept under it
If the host's timeout fires first it cancels the hook and discards its output, and the tool call carries on through the normal permission flow. A timeout is therefore a silent failure of the guard, which is why the hook's own budget is the smaller of the two: it would rather deny than be cancelled.
Verified behaviour
Against a live ZVM 10.9.10, all six rows of the table above. Two worth naming:
jp-ubuntu(healthy, local VPG) tagged checkpoint 7180 and allowed the call, passing the tag back throughadditionalContext.win2019-1(VPGCMH-AWS-1, protected site AWS) was denied: Zerto cannot insert a tagged checkpoint there, so the change would not have been recoverable.
Both decisions held with permission_mode: bypassPermissions. A hook still
blocks when the user has turned permissions off, which is when an agent is most
likely to be running unattended.
Log
~/.zerto-guard-hook.log, or ZERTO_HOOK_LOG. One line per decision.