Concurrent Checkpoint Preflight Side Effects In Tool Executor
Bug
Hermes can mutate checkpoint state for a concurrent tool call before Hermes knows whether that tool call will actually be allowed to execute.
Affected concurrent tool families:
write_file
patch
- destructive
terminal commands
Observed bad outcomes:
- a blocked tool can still create a checkpoint
- a blocked tool can consume the per-turn checkpoint dedup slot for a directory
- a later allowed tool in the same directory and same turn can lose its real pre-change checkpoint because the blocked sibling already consumed it
This is a checkpoint-order bug in Hermes core. It is not a bug where a blocked tool still executes.
System Details
Observed environment for this report:
- OS runtime string:
Microsoft Windows NT 10.0.26200.0
- OS architecture:
64-bit
- shell:
PowerShell 7.6.0
- Hermes version:
0.15.1
- repo branch:
master
- repo commit:
94a70b439ecd514a90420de6f86f43e7e411a1d9
- working directory:
C:\Users\innad\AppData\Local\hermes
- report timestamp:
2026-05-29 20:42:29 +01:00
Where The Bug Is
Primary location:
hermes-agent/agent/tool_executor.py
Failing concurrent path:
- concurrent tool parsing and checkpoint preflight in
hermes-agent/agent/tool_executor.py
Relevant checkpoint manager state:
CheckpointManager.ensure_checkpoint() in hermes-agent/tools/checkpoint_manager.py
Correct reference path for comparison:
- sequential tool execution flow in
hermes-agent/agent/tool_executor.py
Call-order difference:
- the concurrent path performs checkpoint preflight for
write_file, patch, and destructive terminal
- only after that does Hermes evaluate pre-tool blocking and tool guardrails
- the sequential path does the opposite and only checkpoints when execution is not blocked
What Actually Fails
The bug is the order of operations in the concurrent executor.
The concurrent path assumes checkpoint preflight is harmless before execution eligibility is known. That assumption is false because ensure_checkpoint() is stateful. It updates per-turn checkpoint dedup state and can create a real checkpoint before the tool has cleared later block checks.
That creates two failure modes:
- ghost checkpointing: a blocked tool leaves checkpoint side effects even though it never executed
- checkpoint slot stealing: a blocked tool consumes the per-turn checkpoint slot for a directory, causing a later allowed tool in the same directory to skip its real pre-change checkpoint
Evidence Collected
Case 1: Concurrent write_file and patch checkpoint before block evaluation
Observed in hermes-agent/agent/tool_executor.py:
- the concurrent path unwraps tool calls
- it then checkpoints
write_file and patch
- only after that does it compute
block_result and blocked_by_guardrail
Interpretation:
- a concurrent file-mutating tool can touch checkpoint state before Hermes decides whether execution is allowed
Case 2: Concurrent destructive terminal checkpoint before block evaluation
Observed in hermes-agent/agent/tool_executor.py:
- the concurrent path checks destructive
terminal commands
- it calls
ensure_checkpoint(...)
- only after that does it compute
block_result and blocked_by_guardrail
Interpretation:
- the same ordering bug applies to destructive terminal checkpointing, not only file tools
Case 3: Checkpoint dedup state mutates before the snapshot attempt returns
Observed in hermes-agent/tools/checkpoint_manager.py:
new_turn() clears _checkpointed_dirs
ensure_checkpoint() returns early if the directory is already present
- otherwise it adds the normalized directory to
_checkpointed_dirs
- only then does it call
_take(...)
Interpretation:
- checkpoint preflight is not read-only
- once a blocked concurrent call reaches
ensure_checkpoint(), the per-turn dedup state can already be consumed even if the tool never runs
Case 4: Sequential path already uses the correct order
Observed in hermes-agent/agent/tool_executor.py:
- the sequential path wraps checkpoint creation in
if not _execution_blocked
Interpretation:
- Hermes already has the correct behavior in the non-concurrent path
- the bug is a concurrent-path ordering drift, not a missing product concept
How To Reproduce
Reproduction Shape
The bug reproduces when all of these are true:
- Hermes receives concurrent tool calls in one batch.
- At least one call targets
write_file, patch, or a destructive terminal command.
- At least one of those calls is blocked after parsing but before execution.
- Another call in the same turn targets the same working directory and actually runs, or checkpoint history is inspected after the blocked call.
Concrete Reproduction
- Prepare a repo or working directory where Hermes checkpointing is enabled.
- Trigger a concurrent batch with two writes against the same working directory.
- Ensure the first write is blocked after parsing but before execution.
- Ensure the second write is allowed and executes.
- Inspect checkpoint history for that directory.
Actual bad results that this shape allows:
- a checkpoint exists even though the blocked tool never executed
- or the allowed second write has no fresh pre-change checkpoint because the blocked first write already consumed the per-turn checkpoint slot
Terminal variant:
- Trigger a concurrent batch containing a destructive
terminal command and another tool for the same working directory.
- Ensure the destructive terminal call is blocked after parsing but before execution.
- Inspect checkpoint history.
Actual bad result:
- checkpoint side effects can still exist for the blocked destructive terminal call
Non-Bug Clarifications
These are not the root cause:
- the blocked tool actually executing
- any single permission policy implementation
- checkpoint manager dedup by itself
The root cause is the concurrent executor calling checkpoint preflight before Hermes finishes deciding whether the tool call is allowed to run.
Fix
Recommended Fix
Make the concurrent executor use the same gating rule the sequential executor already uses: checkpoint only after Hermes has decided the tool call will execute.
Implementation rule:
- Parse and normalize concurrent tool calls.
- Compute all block outcomes first.
- Only for calls with no block outcome, perform checkpoint creation for
write_file, patch, and destructive terminal.
- Leave
CheckpointManager.ensure_checkpoint() stateful behavior unchanged unless a separate checkpoint-manager issue is identified.
Exact Scope
Files to change:
hermes-agent/agent/tool_executor.py
- add or update concurrent executor tests under
hermes-agent/tests/run_agent/
Exact Behavior
Concurrent execution should do this:
- parse the tool call
- resolve any tool-search indirection
- evaluate pre-tool blocking and guardrails
- if blocked:
- do not call
ensure_checkpoint(...)
- return the blocked result
- if allowed:
- call
ensure_checkpoint(...) for write_file, patch, or destructive terminal
- execute the tool normally
Why This Fix Is Correct
It restores the same safety invariant Hermes already enforces in the sequential path:
- blocked tools do not mutate checkpoint state
- only tools that will actually execute can consume the per-turn checkpoint slot or create a checkpoint
It also fixes both visible failure modes with one ordering correction:
- no ghost checkpoints for blocked tools
- no checkpoint slot stealing from later allowed tools in the same directory and turn
Why Not Leave It As-Is
Without this fix, checkpoint history is not a reliable representation of real execution in concurrent runs.
That causes two product problems:
- users can see checkpoint side effects tied to tools that never ran
- recovery quality degrades because a real allowed mutation can lose its pre-change checkpoint if a blocked sibling consumed the slot first
Suggested Tests
Add tests that cover:
- concurrent
write_file blocked before execution does not create or consume a checkpoint
- concurrent
patch blocked before execution does not create or consume a checkpoint
- concurrent destructive
terminal blocked before execution does not create or consume a checkpoint
- concurrent blocked write followed by concurrent allowed write in the same directory still creates a checkpoint for the allowed write
- sequential blocked write remains unchanged and still does not checkpoint
Concurrent Checkpoint Preflight Side Effects In Tool Executor
Bug
Hermes can mutate checkpoint state for a concurrent tool call before Hermes knows whether that tool call will actually be allowed to execute.
Affected concurrent tool families:
write_filepatchterminalcommandsObserved bad outcomes:
This is a checkpoint-order bug in Hermes core. It is not a bug where a blocked tool still executes.
System Details
Observed environment for this report:
Microsoft Windows NT 10.0.26200.064-bitPowerShell 7.6.00.15.1master94a70b439ecd514a90420de6f86f43e7e411a1d9C:\Users\innad\AppData\Local\hermes2026-05-29 20:42:29 +01:00Where The Bug Is
Primary location:
hermes-agent/agent/tool_executor.pyFailing concurrent path:
hermes-agent/agent/tool_executor.pyRelevant checkpoint manager state:
CheckpointManager.ensure_checkpoint()inhermes-agent/tools/checkpoint_manager.pyCorrect reference path for comparison:
hermes-agent/agent/tool_executor.pyCall-order difference:
write_file,patch, and destructiveterminalWhat Actually Fails
The bug is the order of operations in the concurrent executor.
The concurrent path assumes checkpoint preflight is harmless before execution eligibility is known. That assumption is false because
ensure_checkpoint()is stateful. It updates per-turn checkpoint dedup state and can create a real checkpoint before the tool has cleared later block checks.That creates two failure modes:
Evidence Collected
Case 1: Concurrent
write_fileandpatchcheckpoint before block evaluationObserved in
hermes-agent/agent/tool_executor.py:write_fileandpatchblock_resultandblocked_by_guardrailInterpretation:
Case 2: Concurrent destructive
terminalcheckpoint before block evaluationObserved in
hermes-agent/agent/tool_executor.py:terminalcommandsensure_checkpoint(...)block_resultandblocked_by_guardrailInterpretation:
Case 3: Checkpoint dedup state mutates before the snapshot attempt returns
Observed in
hermes-agent/tools/checkpoint_manager.py:new_turn()clears_checkpointed_dirsensure_checkpoint()returns early if the directory is already present_checkpointed_dirs_take(...)Interpretation:
ensure_checkpoint(), the per-turn dedup state can already be consumed even if the tool never runsCase 4: Sequential path already uses the correct order
Observed in
hermes-agent/agent/tool_executor.py:if not _execution_blockedInterpretation:
How To Reproduce
Reproduction Shape
The bug reproduces when all of these are true:
write_file,patch, or a destructiveterminalcommand.Concrete Reproduction
Actual bad results that this shape allows:
Terminal variant:
terminalcommand and another tool for the same working directory.Actual bad result:
Non-Bug Clarifications
These are not the root cause:
The root cause is the concurrent executor calling checkpoint preflight before Hermes finishes deciding whether the tool call is allowed to run.
Fix
Recommended Fix
Make the concurrent executor use the same gating rule the sequential executor already uses: checkpoint only after Hermes has decided the tool call will execute.
Implementation rule:
write_file,patch, and destructiveterminal.CheckpointManager.ensure_checkpoint()stateful behavior unchanged unless a separate checkpoint-manager issue is identified.Exact Scope
Files to change:
hermes-agent/agent/tool_executor.pyhermes-agent/tests/run_agent/Exact Behavior
Concurrent execution should do this:
ensure_checkpoint(...)ensure_checkpoint(...)forwrite_file,patch, or destructiveterminalWhy This Fix Is Correct
It restores the same safety invariant Hermes already enforces in the sequential path:
It also fixes both visible failure modes with one ordering correction:
Why Not Leave It As-Is
Without this fix, checkpoint history is not a reliable representation of real execution in concurrent runs.
That causes two product problems:
Suggested Tests
Add tests that cover:
write_fileblocked before execution does not create or consume a checkpointpatchblocked before execution does not create or consume a checkpointterminalblocked before execution does not create or consume a checkpoint