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.
situation
decision
effect
tool in read_only_tools
none
runs, no checkpoint
mutating, checkpoint confirmed
none + 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
nothing to rewind to; human decides
mutating, no VM in the arguments
prompt
the catalog's vm_arg missed
unlisted tool
prompt
nobody said it was read-only
Verified against a live ZVM 10.9.10
jp-ubuntu (healthy local VPG) → tagged checkpoint 7180, allowed, tag passed back via additionalContext
win2019-1 (VPG CMH-AWS-1, protected site AWS) → denied. The deny path works; see the known issue below, because that particular denial was wrong.
Both held under permission_mode: bypassPermissions. A hook still blocks when the user has turned permissions off, which is exactly when an agent is most likely running unattended.
Correction: the AWS denial was a false negative
An earlier version of this description said win2019-1 was denied because an AWS-protected VPG cannot be tagged. That is not true on 10.9.10, and it is not what happened.
The insert succeeded. The checkpoint is in the journal as cp 56, stamped about a minute after the hook had already reported failure. What actually happened is that wait_for_tag gave up after its hardcoded 45s.
That budget is generous on a VPG that checkpoints every 5s and far too short on one that checkpoints every 630s. Journal cadence is set by the protected site:
protected at
journal gap
tag visible after
vSphere
5s
~4s
Azure
60s
~34s
AWS
630s
~128s
So this hook currently false-denies on cloud-protected VPGs: it refuses legitimate work while Zerto is in the middle of creating the very checkpoint it claims is missing. That failure is worse than the one the hook guards against, because it is silent and the log reads like the guard is working.
hooks/README.md now records this as a known issue and says to scope the matcher to vSphere-protected workloads until wait_for_tag becomes cadence-aware. The fix is a behaviour change and belongs in its own PR.
The docs correction for the same wrong claim elsewhere in the repo is #11.
Details worth reviewing
Decisions go out as JSON, not exit 2. Exit 2 blocks unconditionally but discards the JSON, and the reason with it, so the model would be refused without being told why. The JSON path passes the Zerto error through verbatim.
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, known-but-untaggable denies.
Timeouts. A successful tag took ~4s; a refusal took 63s (the 45s wait_for_tag budget plus overhead). If the host's timeout fires first it cancels the hook and discards its output, and the call proceeds unguarded — a silent guard failure. So the hook's own budget (150s) stays under the configured one (180s): better to deny than be cancelled.
Broad excepts in hooks/ are deliberate and scoped in pyproject.toml. A hook that raises breaks the tool call it exists to protect.
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.
| situation | decision | effect |
|---|---|---|
| tool in `read_only_tools` | none | runs, no checkpoint |
| mutating, checkpoint confirmed | none + `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` | nothing to rewind to; human decides |
| mutating, no VM in the arguments | `prompt` | the catalog's `vm_arg` missed |
| unlisted tool | `prompt` | nobody said it was read-only |
## Verified against a live ZVM 10.9.10
- `jp-ubuntu` (healthy local VPG) → tagged **checkpoint 7180**, allowed, tag passed back via `additionalContext`
- `win2019-1` (VPG `CMH-AWS-1`, protected site AWS) → **denied**. The deny path works; see the known issue below, because that particular denial was wrong.
Both held under `permission_mode: bypassPermissions`. **A hook still blocks when the user has turned permissions off**, which is exactly when an agent is most likely running unattended.
## Correction: the AWS denial was a false negative
An earlier version of this description said `win2019-1` was denied because an AWS-protected VPG cannot be tagged. **That is not true on 10.9.10, and it is not what happened.**
The insert succeeded. The checkpoint is in the journal as `cp 56`, stamped about a minute after the hook had already reported failure. What actually happened is that `wait_for_tag` gave up after its hardcoded 45s.
That budget is generous on a VPG that checkpoints every 5s and far too short on one that checkpoints every 630s. Journal cadence is set by the **protected** site:
| protected at | journal gap | tag visible after |
|---|---|---|
| vSphere | 5s | ~4s |
| Azure | 60s | ~34s |
| AWS | 630s | ~128s |
So **this hook currently false-denies on cloud-protected VPGs**: it refuses legitimate work while Zerto is in the middle of creating the very checkpoint it claims is missing. That failure is worse than the one the hook guards against, because it is silent and the log reads like the guard is working.
`hooks/README.md` now records this as a known issue and says to scope the matcher to vSphere-protected workloads until `wait_for_tag` becomes cadence-aware. The fix is a behaviour change and belongs in its own PR.
The docs correction for the same wrong claim elsewhere in the repo is #11.
## Details worth reviewing
**Decisions go out as JSON, not exit 2.** Exit 2 blocks unconditionally but discards the JSON, and the reason with it, so the model would be refused without being told why. The JSON path passes the Zerto error through verbatim.
**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, known-but-untaggable denies.
**Timeouts.** A successful tag took ~4s; a refusal took 63s (the 45s `wait_for_tag` budget plus overhead). If the host's timeout fires first it cancels the hook and **discards its output**, and the call proceeds unguarded — a silent guard failure. So the hook's own budget (150s) stays under the configured one (180s): better to deny than be cancelled.
**Broad excepts in `hooks/` are deliberate** and scoped in `pyproject.toml`. A hook that raises breaks the tool call it exists to protect.
`pytest`: 60 passed (12 new). `ruff check` clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
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
hooks/README.md said the win2019-1 denial happened because Zerto cannot
insert a tagged checkpoint on an AWS-protected VPG. That is not what
happened, and it is not true on 10.9.10.
The insert succeeded. The checkpoint is in the journal as cp 56, stamped
about a minute after the hook had already reported failure. What actually
happened is that wait_for_tag gave up after its hardcoded 45s, which is
generous on a VPG that checkpoints every 5s and far too short on one that
checkpoints every 630s.
So the documented "verified" example was a false denial. The deny path
works; that particular denial was wrong. Recorded as a known issue,
because a guard that silently refuses legitimate work on every
cloud-protected VM while looking correct is a worse failure than the one
it exists to prevent.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The catalog and
zerto_check_toolare 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
PreToolUsehook 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_toolsadditionalContextdenypromptpromptvm_argmissedpromptVerified against a live ZVM 10.9.10
jp-ubuntu(healthy local VPG) → tagged checkpoint 7180, allowed, tag passed back viaadditionalContextwin2019-1(VPGCMH-AWS-1, protected site AWS) → denied. The deny path works; see the known issue below, because that particular denial was wrong.Both held under
permission_mode: bypassPermissions. A hook still blocks when the user has turned permissions off, which is exactly when an agent is most likely running unattended.Correction: the AWS denial was a false negative
An earlier version of this description said
win2019-1was denied because an AWS-protected VPG cannot be tagged. That is not true on 10.9.10, and it is not what happened.The insert succeeded. The checkpoint is in the journal as
cp 56, stamped about a minute after the hook had already reported failure. What actually happened is thatwait_for_taggave up after its hardcoded 45s.That budget is generous on a VPG that checkpoints every 5s and far too short on one that checkpoints every 630s. Journal cadence is set by the protected site:
So this hook currently false-denies on cloud-protected VPGs: it refuses legitimate work while Zerto is in the middle of creating the very checkpoint it claims is missing. That failure is worse than the one the hook guards against, because it is silent and the log reads like the guard is working.
hooks/README.mdnow records this as a known issue and says to scope the matcher to vSphere-protected workloads untilwait_for_tagbecomes cadence-aware. The fix is a behaviour change and belongs in its own PR.The docs correction for the same wrong claim elsewhere in the repo is #11.
Details worth reviewing
Decisions go out as JSON, not exit 2. Exit 2 blocks unconditionally but discards the JSON, and the reason with it, so the model would be refused without being told why. The JSON path passes the Zerto error through verbatim.
A lookup miss is not a refusal. Asking Zerto for a plain hostname by
vmIdentifierreturns 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, known-but-untaggable denies.Timeouts. A successful tag took ~4s; a refusal took 63s (the 45s
wait_for_tagbudget plus overhead). If the host's timeout fires first it cancels the hook and discards its output, and the call proceeds unguarded — a silent guard failure. So the hook's own budget (150s) stays under the configured one (180s): better to deny than be cancelled.Broad excepts in
hooks/are deliberate and scoped inpyproject.toml. A hook that raises breaks the tool call it exists to protect.pytest: 60 passed (12 new).ruff checkclean.🤖 Generated with Claude Code
https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
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_016yVfC5nvZowoLFnEGWhLGn735c6c4a64to0263f3797a