# zsnoop-mcp [![PyPI](https://img.shields.io/pypi/v/zsnoop-mcp.svg)](https://pypi.org/project/zsnoop-mcp/) [![Python](https://img.shields.io/pypi/pyversions/zsnoop-mcp.svg)](https://pypi.org/project/zsnoop-mcp/) [![License: MIT](https://img.shields.io/pypi/l/zsnoop-mcp.svg)](https://github.com/hamsolodev/zsnoop-mcp/blob/main/LICENSE) [![CI](https://github.com/hamsolodev/zsnoop-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/hamsolodev/zsnoop-mcp/actions/workflows/ci.yml) **Ask your AI assistant things like:** - โช *"Recover my `.zshrc` from before I committed the rewrite three weeks ago."* - ๐Ÿงน *"Which snapshots older than 6 months are wasting the most space?"* - ๐Ÿ”Ž *"When did the directory `/srv/backups` first appear on this host?"* - โŒ› *"Find everything deleted under `/home/youruser` in 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 **ZFS snapshot exploration and recovery on remote hosts** โ€” read-only by default, with opt-in `restore_*` tools (v0.4.0+) gated per host. 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. Read-only by default; the writable `restore_file` / `restore_dir` tools (v0.4.0+) are opt-in per host and bounded by an operator-defined path allowlist โ€” with the stock config, nothing on the remote can be written. ## Quickstart ```sh # 1. Install uv tool 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 by default**, enforced by an explicit allowlist on the agent side. The writable `restore_file` / `restore_dir` tools (v0.4.0+) are opt-in per host and bounded by an operator-defined path allowlist; with the default config they refuse before doing anything. ## About this codebase This project was developed collaboratively with [Claude Code](https://claude.com/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-default was a hard requirement from day one, enforced by an explicit method allowlist and the test suite. The opt-in `restore_*` tools added in v0.4.0 are the only writable methods and are gated server-side on per-host config (off by default; requires a non-empty path allowlist when enabled) โ€” see [SECURITY.md](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/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 ```text โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 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 four dominant workflows: **file recovery** ("get me /etc/foo as it was yesterday" โ€” to your workstation, or restored in place on the server), **config drift audit** ("when did X change?"), **forensics** ("what was on the box when Y broke?"), and **storage housekeeping** ("which snapshots are biggest / oldest?"). The tools are grouped below by their dominant use case; many compose across workflows. ### Discovery & introspection | 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 | ### Snapshot inventory & housekeeping | Tool | What it does | | ---------------------- | -------------------------------------------------------- | | `list_snapshots` | Snapshots (optional dataset/time/cap filters) | | `snapshot_cadence` | Summary: counts by class, gap (per-dataset), span | | `stale_snapshots` | Snapshots older than a time phrase, sorted by uniqueness | | `size_delta` | Bytes written between two snapshots of one dataset | ### Browsing & sizing within a snapshot | Tool | What it does | | ---------------------- | -------------------------------------------------------- | | `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 | ### Reading content | Tool | What it does | | ---------------------- | -------------------------------------------------------- | | `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 | | `checksum_file` | Full-file SHA-256 (256 MiB cap) for integrity checks | ### Comparing snapshots & tracing change | Tool | What it does | | ---------------------- | -------------------------------------------------------- | | `diff_snapshots` | Path-level diff between two snapshots | | `file_diff` | Unified diff of one file across two snapshots | | `file_history` | Every snapshot's version of a given file in a dataset | | `versions_of` | `file_history` deduped by hash (distinct versions only) | | `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; reveals when deleted | | `find_deleted` | Paths deleted between two snapshots in a time window | | `bisect_change` | Binary-search for the snapshot where a predicate flips | ### Recovery โ€” copy out to your workstation | Tool | What it does | | ---------------------- | -------------------------------------------------------- | | `fetch_file` | Copy a snapshot file to a local path via SFTP | | `fetch_dir` | Copy a snapshot directory tree to a local path via SFTP | ### Recovery โ€” restore in place on the server (opt-in, v0.4.0+) These are the only **writable** tools. Disabled per host by default; require `allow_restore = true` and a non-empty `restore_paths` allowlist in `hosts.toml`. See [SECURITY.md](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/SECURITY.md) for the threat model. | Tool | What it does | | ---------------------- | -------------------------------------------------------- | | `restore_file` | Restore a snapshot file to a server path (opt-in) | | `restore_dir` | Restore a snapshot directory tree to a server path | 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) ```sh uv tool install zsnoop-mcp ``` Run it with `zsnoop-mcp`. ### From a clone (for hacking on the code) ```sh 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](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/PUBLISHING.md) for the per-release flow (version bump โ†’ tag โ†’ CI publishes via OIDC). ## Configure Create `~/.config/zsnoop-mcp/hosts.toml`: ```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): ```sh # user mode: grant diff for each pool you want to compare snapshots in sudo zfs allow -u $USER diff rpool ``` See [docs/INSTALL.md](https://github.com/hamsolodev/zsnoop-mcp/blob/main/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`: ```sh 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 ` instead: ```sh 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: ```jsonc { "mcpServers": { "zsnoop": { "command": "zsnoop-mcp" } } } ``` ## Use See [docs/USAGE.md](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/USAGE.md) for example prompts that exercise the file-recovery, drift-audit, and forensics workflows. ## Documentation - **New here?** Start with the [onboarding tutorial](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/onboarding/index.md) โ€” 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](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/INSTALL.md) โ€” local setup, ZFS delegation, sudo mode - [Usage examples](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/USAGE.md) โ€” concrete prompts the tools handle - [Security model](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/SECURITY.md) โ€” threat model, guarantees, sudo tradeoff - [Publishing](https://github.com/hamsolodev/zsnoop-mcp/blob/main/docs/PUBLISHING.md) โ€” releasing to PyPI ## Development ```sh 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](https://github.com/hamsolodev/zsnoop-mcp/blob/main/LICENSE).