MCP server for read-only exploration of ZFS snapshots on remote hosts over SSH (AI-assisted)
Project description
zsnoop-mcp
Ask your AI assistant things like:
- โช "Recover my
.zshrcfrom before I committed the rewrite three weeks ago." - ๐งน "Which snapshots older than 6 months are wasting the most space?"
- ๐ "When did the directory
/srv/backupsfirst appear on this host?" - โ "Find everything deleted under
/home/youruserin the last week, and show me when each thing was last present." - ๐ฅ "Are any of my pools throwing disk errors? When was the last scrub?"
An MCP server for read-only exploration of ZFS snapshots on remote hosts.
Browse, diff, search, and read files from any snapshot on any of your ZFS hosts through your AI assistant, over a single persistent SSH connection per host. No mutation operations are ever exposed.
Quickstart
# 1. Install
uv tool install zsnoop-mcp # or: pipx install zsnoop-mcp
# 2. Configure one host (more in docs/INSTALL.md)
mkdir -p ~/.config/zsnoop-mcp
cat > ~/.config/zsnoop-mcp/hosts.toml <<'EOF'
[hosts.myhost]
ssh_target = "myhost.example.com"
agent_mode = "bootstrap"
sudo = false
EOF
# 3. Register the MCP server with Claude Code
claude mcp add zsnoop --scope user -- zsnoop-mcp
# 4. Restart Claude Code, then ask your assistant any of the prompts above.
The agent is streamed over SSH on first connect โ nothing needs to be
installed on the remote host beyond python3 (3.11+) and the zfs CLI.
Read-only is enforced by an explicit allowlist on the agent side; the
LLM can't bypass it.
About this codebase
This project was developed collaboratively with Claude Code (Anthropic). The human author (Mark Hellewell) defined the architecture, security model, and acceptance criteria, and reviewed every change before it landed; Claude handled the bulk of the drafting, test scaffolding, refactors, and documentation. Read-only-by-construction was a hard requirement from day one, enforced by an explicit method allowlist and the test suite โ see SECURITY.md. If you're reviewing or auditing the code, treat that as context, not as a reason to skip the usual scrutiny.
How it works
โโโโโโโโโโโโโโโโโโโ MCP (stdio) โโโโโโโโโโโโโโโโโโโโโโ
โ MCP client โ โโโโโโโโโโโโโโโโบ โ zsnoop-mcp server โ
โ (Claude Code,โฆ) โ โโโโโโโโโโโโโโโโ โ (local) โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโ
โ
JSON-RPC over SSH stdio โ one persistent
(one channel per host) โ subprocess
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ zfs-snoop-agent โ
โ (remote, Python) โ
โโโโโโโโโโโฌโโโโโโโโโโโโ
โ
zfs list / zfs diff,
walk .zfs/snapshot/โฆ
โผ
ZFS pool
The remote agent is a single-file, stdlib-only Python script. It can be
pre-installed at ~/bin/zfs-snoop-agent on each host, or streamed over SSH
stdin on each connection โ no permanent install required.
Tools exposed to the LLM
Designed around three dominant workflows: file recovery ("get me /etc/foo as it was yesterday"), config drift audit ("when did X change?"), and forensics ("what was on the box when Y broke?").
| Tool | What it does |
|---|---|
list_hosts |
Configured hosts |
agent_info |
Agent version, methods, limits |
list_pools |
ZFS pools visible to the agent (live discovery) |
pool_status |
Parsed zpool status: vdev tree, scrub, errors |
list_datasets |
Filesystems and volumes |
dataset_properties |
zfs get (all or filtered) with values + sources |
list_snapshots |
Snapshots (optionally scoped to a dataset, recursive) |
snapshot_cadence |
Snapshot inventory summary: counts by class, biggest gap |
diff_snapshots |
Path-level diff between two snapshots |
list_dir |
Bounded directory listing within a snapshot |
size_breakdown |
Recursive bytes for a snapshot dir + per-child sizes |
top_consumers |
Top-N largest files/dirs under a snapshot subtree |
read_file |
Bounded read; UTF-8 or base64 for binary |
find_files |
fnmatch name search inside a snapshot |
content_grep |
Regex content search inside a snapshot |
file_history |
Every snapshot's version of a given file in a dataset |
versions_of |
file_history deduped by content hash (distinct versions only) |
file_diff |
Unified diff of one file across two snapshots |
snapshots_containing |
Snapshots in which a path currently exists (time-ranged) |
first_appearance |
Earliest snapshot containing a path |
last_appearance |
Latest snapshot containing a path (answers "when did X disappear?") |
find_deleted |
Paths deleted between two snapshots in a time window |
bisect_change |
Binary-search snapshots for the one where a predicate flips |
stale_snapshots |
Snapshots older than a time phrase, sorted by unique bytes |
size_delta |
Bytes written between two snapshots of one dataset |
Time-range parameters accept ISO 8601 or human phrases โ yesterday,
last week, 3 days ago, 2 hours ago, etc. Parsing happens locally; the
agent only sees absolute ISO 8601 timestamps.
Install
From PyPI (recommended)
uv tool install zsnoop-mcp # or: pipx install zsnoop-mcp / pip install zsnoop-mcp
Run it with zsnoop-mcp.
From a clone (for hacking on the code)
git clone https://github.com/hamsolodev/zsnoop-mcp.git
cd zsnoop-mcp
uv sync
Run it with uv run zsnoop-mcp from the checkout.
See docs/PUBLISHING.md for the per-release flow (version bump โ tag โ CI publishes via OIDC).
Configure
Create ~/.config/zsnoop-mcp/hosts.toml:
[hosts.r2d2]
ssh_target = "r2d2.example.com"
agent_mode = "bootstrap" # or "preinstalled"
sudo = false # set true to read root-owned snapshot files
pools = ["rpool", "bpool"] # used by the LLM for scoping hints
[hosts.c3po]
ssh_target = "c3po.example.com"
agent_mode = "bootstrap"
sudo = false
pools = ["rpool"]
[hosts.this-box]
transport = "local" # run the agent on this machine, no SSH
agent_mode = "bootstrap"
Per-host setup on the remote (one-time):
# user mode: grant diff for each pool you want to compare snapshots in
sudo zfs allow -u $USER diff rpool
See docs/INSTALL.md for the full setup, including sudo mode for reading root-owned snapshot files.
Wire into Claude Code
After uv tool install zsnoop-mcp:
claude mcp add zsnoop --scope user -- zsnoop-mcp
That writes the entry directly to your Claude Code config; no JSON
editing needed. Restart your Claude Code session; the tools appear
under the zsnoop namespace.
If you're running from a worktree instead of an installed binary, point
the command at uv run --directory <path> instead:
claude mcp add zsnoop --scope user -- \
uv run --directory ~/path/to/zsnoop-mcp zsnoop-mcp
Or, if you'd rather edit ~/.claude/settings.json by hand:
{
"mcpServers": {
"zsnoop": { "command": "zsnoop-mcp" }
}
}
Use
See docs/USAGE.md for example prompts that exercise the file-recovery, drift-audit, and forensics workflows.
Documentation
- New here? Start with the onboarding tutorial โ
a 10-chapter, what/why/how walk through the codebase, ending with a
worked example of adding a new tool end-to-end. Renders nicely as HTML
via
uv run mkdocs serve(see--group docs). - Installation โ local setup, ZFS delegation, sudo mode
- Usage examples โ concrete prompts the tools handle
- Security model โ threat model, guarantees, sudo tradeoff
- Publishing โ releasing to PyPI
Development
uv sync # install runtime + dev deps into .venv
uv run pytest # tests
uv run ruff check # lint
uv run ruff format # format
uv run mypy # type-check
uv run pip-audit --skip-editable # CVE scan of locked deps
uv run pre-commit install # set up hooks
Pre-commit runs pip-audit automatically whenever pyproject.toml or
uv.lock change.
License
MIT โ see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zsnoop_mcp-0.1.2.tar.gz.
File metadata
- Download URL: zsnoop_mcp-0.1.2.tar.gz
- Upload date:
- Size: 216.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56c89a81bdf607cc353a0ceb07b766a1014ac0eea2ff450fc2cbba75041eb106
|
|
| MD5 |
5d490537bf98b1bd68a3aa5ec5efd911
|
|
| BLAKE2b-256 |
89c4ce48686142c2a44c6ba2ac8a7c07671d8b90c9ae7c646593cfa7dfefde2e
|
Provenance
The following attestation bundles were made for zsnoop_mcp-0.1.2.tar.gz:
Publisher:
release.yml on hamsolodev/zsnoop-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zsnoop_mcp-0.1.2.tar.gz -
Subject digest:
56c89a81bdf607cc353a0ceb07b766a1014ac0eea2ff450fc2cbba75041eb106 - Sigstore transparency entry: 1631454659
- Sigstore integration time:
-
Permalink:
hamsolodev/zsnoop-mcp@c339dd342d0bdcce7fd3eaa9b08a3e514004d982 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/hamsolodev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c339dd342d0bdcce7fd3eaa9b08a3e514004d982 -
Trigger Event:
push
-
Statement type:
File details
Details for the file zsnoop_mcp-0.1.2-py3-none-any.whl.
File metadata
- Download URL: zsnoop_mcp-0.1.2-py3-none-any.whl
- Upload date:
- Size: 42.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82f6130de171487b3b94ef9cd1f1c7c2c66e8a4da7041e4a108780cf6b52fa5f
|
|
| MD5 |
7e757e4913514902546a9b0afa158cd8
|
|
| BLAKE2b-256 |
bafac4a836e669998416a2c2ecd1288f77226e62e830fe5de25914396c11295b
|
Provenance
The following attestation bundles were made for zsnoop_mcp-0.1.2-py3-none-any.whl:
Publisher:
release.yml on hamsolodev/zsnoop-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zsnoop_mcp-0.1.2-py3-none-any.whl -
Subject digest:
82f6130de171487b3b94ef9cd1f1c7c2c66e8a4da7041e4a108780cf6b52fa5f - Sigstore transparency entry: 1631454678
- Sigstore integration time:
-
Permalink:
hamsolodev/zsnoop-mcp@c339dd342d0bdcce7fd3eaa9b08a3e514004d982 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/hamsolodev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c339dd342d0bdcce7fd3eaa9b08a3e514004d982 -
Trigger Event:
push
-
Statement type: