This release is a pre-release and may not be stable for production use.
agent-handshake
Check that what your agent is told matches the tools it actually has
An agent's instructions and tool descriptions often name its tools, and nothing checks that those names still exist. When a tool is renamed or an MCP server adds a prefix, the model calls a tool it does not have, gets an error and often just gives up.
agent-handshake reads the request your agent sends to the model and reports where the text and the tools disagree, without calling a model.
Example
The instructions say mem_ssh; the MCP server registered the tool as
mcp__mem__mem_ssh (a real case):
{
"model": "claude-haiku-4-5",
"max_tokens": 1024,
"system": "Use `mem_ssh` to connect to Tower.",
"messages": [{"role": "user", "content": "Is Tower up?"}],
"tools": [{"name": "mcp__mem__mem_ssh", "input_schema": {"type": "object"}}]
}
$ handshake check request.json
3 rules enabled. Checked request.json (Anthropic Messages): 1 tools, 2 text segments.
✗ HS002 request.json (system): Text names `mem_ssh`; no tool has that exact name. Did you mean `mcp__mem__mem_ssh`?
1 findings (1 errors, 0 warnings).
Install
pip install --pre agent-handshake
Status: beta (1.0.0b2). --pre is needed while 1.0 is in beta: without it
pip installs 0.10.0, an older and different tool (see History).
Get request.json
request.json is the request body as sent to the model, in OpenAI Chat
Completions, OpenAI Responses or Anthropic Messages format. If your code calls
an SDK, put the call's arguments in a dict, save it as JSON and pass the same
dict to the call.
OpenAI:
import json
from openai import OpenAI
request = {
"model": "gpt-4.1",
"messages": [
{"role": "system", "content": "Use `get_weather` for questions about the weather."},
{"role": "user", "content": "Is it raining in Lisbon?"},
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
],
}
with open("request.json", "w") as f:
json.dump(request, f, indent=2)
response = OpenAI().chat.completions.create(**request)
Anthropic:
import json
from anthropic import Anthropic
request = {
"model": "claude-sonnet-5-5",
"max_tokens": 1024,
"system": "Use `get_weather` for questions about the weather.",
"messages": [{"role": "user", "content": "Is it raining in Lisbon?"}],
"tools": [
{
"name": "get_weather",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
],
}
with open("request.json", "w") as f:
json.dump(request, f, indent=2)
response = Anthropic().messages.create(**request)
Run
handshake check request.json
Each finding names the rule, where the text is (its path in the request) and
the closest real name. Errors make the command exit with code 1; warnings do
not, unless --strict is given. --config points to a handshake.yaml or
pyproject.toml; without it, the one in the current directory is used.
In pytest
agent_handshake.check(body, config=None) runs the same rules on a dict and
returns the findings (since 1.0.0b2).
import json
from agent_handshake import check
def test_request_matches_tools():
with open("request.json") as fp:
body = json.load(fp) # or build it with your agent's own code
assert not [f for f in check(body) if f.severity == "error"]
A complete example: examples/pytest/.
GitHub Actions
- run: |
pip install --pre agent-handshake
handshake check request.json
Rules
| rule | finds | default |
|---|---|---|
| HS001 | a tool named in a form that can only be a tool, that the request does not have | on, warning |
| HS002 | a tool name that is almost right (case, -/_, prefix, suffix) |
on, error |
| HS003 | an argument the tool's schema does not have | on, error |
| HS004 | an example value outside a field's allowed values | off |
| HS005 | example JSON keys that do not match the call or response format | off |
| HS007 | prose that may name a tool the request does not have | off |
Rule pages: docs/rules/. Turning rules off, changing their severity and ignoring names: docs/configuration.md.
Known limitations
HS001 can warn about names that are not missing tools:
- Code Mode declarations. A description that declares functions as
tools.x({ ... })is read as calls tox. - Negative forms. "Never attempt 'apply_patch' ... they don't exist" is
read as naming
apply_patch. - Placeholders. "I'm going to use
tool_name" is read as namingtool_name.
HS001 findings are warnings, so they do not fail the check unless --strict
is used. To silence them, ignore the names or turn the rule off in
handshake.yaml (or [tool.handshake] in pyproject.toml):
ignore:
- tool_name # never report this name
- {rule: HS001, ref: apply_patch}
rules:
HS001: {enabled: false} # or turn HS001 off
Benchmark
Measured once on the frozen configuration, on three sets that share no project: dev (used to build the rules), holdout and holdout-2 (never used to tune them). Recall is over real, fixed mismatches from the projects' history; precision is over hand-labelled findings on the current code of open-source agents. Small samples are marked by their size.
| dev | holdout | holdout-2 | |
|---|---|---|---|
| recall cases (projects) | 25 (12) | 14 (10) | 4 (4) |
| catchable cases caught by rules on by default | 8/11 | 4/11 | 2/2 |
| precision corpus: repositories (requests) | 4 (13) | 3 (12) | 4 (16) |
| HS001 precision | 13/14 | 0/24 | no findings |
| HS002 precision | 11/11 | no findings | 5/5 |
| HS003 precision | 2/2 | no findings | no findings |
| known mismatches in the corpus found | 22/32 | 6/24 | 6/17 |
The holdout HS001 findings are Code Mode function declarations of one project
(tools.x({ ... })), which HS001 reads as calls. Details and the method:
benchmarks/README.md; run it with make benchmark.
Roadmap
Capturing requests from a running agent, so there is no request.json to
write by hand (not started).
Contributing
See CONTRIBUTING.md.
License
Apache 2.0. See LICENSE.
History (0.x)
Releases up to 0.10.0 on PyPI are a different, closed tool: contract tests between agents. Its code is in tag contract-testing-final; see why it was closed.
Metadata
Release files for agent-handshake 1.0.0b2
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 | |
|---|---|---|---|---|
| agent_handshake-1.0.0b2.tar.gz | 1.8 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_handshake-1.0.0b2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.8 MB
Release files / agent_handshake-1.0.0b2.tar.gz
| Download URL | agent_handshake-1.0.0b2.tar.gz |
|---|---|
| Size | 1.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a67e6cdce727176051c7dca1663e311bcd2aa8eacc4a46ad3a88984e6b0303c3
|
|
BLAKE2b-256 checksum How to use checksums |
c7ba8d5e83cf85c7c400d6ae89ffb84fd3fec2ec215ed415057dc92013b97d59
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / agent_handshake-1.0.0b2-py3-none-any.whl
| Download URL | agent_handshake-1.0.0b2-py3-none-any.whl |
|---|---|
| Size | 39.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7b0714ed78e6c10b00be88fdf797ff81984bbafff2f385436e2b139c72445ccb
|
|
BLAKE2b-256 checksum How to use checksums |
984757883ca67dca77fa4b48cb9bce0174fabb7a5677db10c8960ca60e530250
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for agent-handshake 1.0.0b2
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for agent_handshake-1.0.0b2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA 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.