Show HN: Git log for your infrastructure (including your AI agent's config)
statedrift is an open-source tool that records Linux host state (network, packages, services, etc.) into a tamper-evident hash chain, enabling version control and auditability. It supports snapshots, diffs, security analysis, and cryptographic integrity verification without any external dependencies or cloud services.
Uh oh!
There was an error while loading. Please reload this page.
Notifications You must be signed in to change notification settings
Fork 0
Star 0
BranchesTags
Open more actions menu
Folders and files
NameName
Last commit message
Last commit date
Latest commit
History
57 Commits
57 Commits
.github/workflows
.github/workflows
cmd/statedrift
cmd/statedrift
configs
configs
demo
demo
docs
docs
internal
internal
tests
tests
.dockerignore
.dockerignore
.gitignore
.gitignore
CHANGELOG.md
CHANGELOG.md
Dockerfile
Dockerfile
LICENSE
LICENSE
Makefile
Makefile
README.md
README.md
ROADMAP.md
ROADMAP.md
go.mod
go.mod
install.sh
install.sh
Repository files navigation
Git log for your infrastructure. A single Go binary that snapshots what your Linux hosts actually are — network, packages, services, users, sudoers, kernel modules, mounts, firewall, containers, GPUs, and more — into a tamper-evident hash chain. Diff any two points in time, and hand auditors a cryptographically verifiable evidence bundle.
Linux · single static binary · no daemon required · no cloud · zero third-party dependencies.
See it work
$ sudo statedrift init ✓ Store initialized at /var/lib/statedrift/chain ✓ Genesis snapshot recorded ✓ Run 'statedrift snap' to take more snapshots.
...an hour later, something changed on the box. Snapshot again:
$ sudo statedrift snap ✓ Snapshot recorded
What actually changed?
$ statedrift diff HEAD~1 HEAD Comparing 2026-06-30 14:00 → 2026-06-30 15:00
kernel_params: ~ net.ipv4.ip_forward: "0" → "1"
users: + backdoor: uid=1001 gid=1001 shell=/bin/bash
2 material changes, 0 counter increments
Someone turned the host into a router and gave themselves an account — surfaced in plain sight, with a timestamped record of exactly when it happened.
A diff lists everything that changed. analyze runs the built-in rule engine over that diff and tells you which of those changes are dangerous, and how severe:
$ statedrift analyze statedrift analyze — 2026-06-30T14:00:00Z → 2026-06-30T15:00:00Z 2 material changes, 54 rules evaluated
[HIGH] Kernel parameter changed (1 match) A sysctl value was changed. Security-relevant params (ip_forward, rp_filter) are high severity. [HIGH] New user account (1 match) A new entry was added to /etc/passwd. New accounts created outside a change window may be backdoors.
2 finding(s). Run 'statedrift diff HEAD~1 HEAD' for full details. Pro rules skipped (no license). See 'statedrift help analyze'.
And because every snapshot is SHA-256 hash-chained to the one before it, nobody can quietly rewrite history to cover their tracks — verify catches an edited snapshot:
$ statedrift verify Verifying chain integrity... Snapshots: 3 Chain: ✓ all 3 hashes valid Head: ✓ matches last snapshot Result: INTEGRITY VERIFIED
No tampering detected. All snapshots are consistent with their recorded hashes.
...but if an attacker edits an old snapshot to erase the evidence:
$ statedrift verify Verifying chain integrity... Snapshots: 3 Chain: ✗ BREAK at snapshot #1 (2026-06-30T15:00:00Z) Expected prev_hash: a3f81c2d9e4b5f6a... Found prev_hash: 7c0091a2bd34ef58... Result: INTEGRITY VIOLATION
The problem
During an incident, nobody can agree on what the config looked like before things broke. During an audit, your team burns weeks on screenshots and spreadsheets that auditors don't trust — because files can be edited without a trace. Drift happens silently, and by the time you find it, there's no record of when it started.
statedrift records what your infrastructure actually is — not what it's supposed to be — in a tamper-evident hash chain that makes retroactive edits detectable.
Install
curl -fsSL https://raw.githubusercontent.com/statedrift/statedrift/main/install.sh | bash
Linux only (amd64 and arm64). Needs curl (or wget), tar, and sha256sum. The installer pulls the latest release, verifies its SHA-256 checksum, and installs to /usr/local/bin.
Pin a version, or install without sudo to a user-writable prefix:
curl -fsSL https://raw.githubusercontent.com/statedrift/statedrift/main/install.sh | bash -s -- --version 0.8.1 curl -fsSL https://raw.githubusercontent.com/statedrift/statedrift/main/install.sh | bash -s -- --prefix "$HOME/.local/bin"
Or build from source (Go, no external dependencies):
make build && sudo cp bin/statedrift /usr/local/bin/
Quick start — 60 seconds to your first drift catch
sudo statedrift init # record the genesis snapshot (chain root) sudo statedrift snap # ...later, snapshot again after any change sudo statedrift diff HEAD~1 HEAD # see exactly what changed sudo statedrift verify # prove the whole chain is untampered
Prefer to try it without root? Use a home-directory store — then no command needs sudo:
export STATEDRIFT_STORE=$HOME/.statedrift statedrift init && statedrift snap
That's the loop. Run snap on a schedule (cron, or the built-in statedrift watch) and you've got a continuous, verifiable record of everything your host is. Want more than the core sections? Optional collectors (containers, GPUs, your AI agent's config, ...) are one command away:
sudo statedrift config enable containers # or: gpu, dataplane, harness, ... or all
See what gets captured below, or statedrift --help for every command.
Optional: shell alias
statedrift is deliberately unambiguous in scripts and logs. For interactive use, add an alias to your shell profile:
~/.bashrc or ~/.zshrc
alias sd='statedrift'
If you're working with a non-default store path, bake it in:
alias sd='STATEDRIFT_STORE=/var/lib/statedrift statedrift'
Note: sd is also the name of an unrelated find-and-replace tool. If you have both installed, choose a different alias name (e.g. sdt) to avoid the conflict.
What gets captured
Every snapshot records:
Section What Source
host Hostname, OS, kernel version, uptime /proc/version, /etc/os-release
network.interfaces IPs, link state, MTU, packet counters /sys/class/net/
network.routes Routing table (destination, gateway, metric) /proc/net/route
network.dns Nameservers, search domains /etc/resolv.conf
kernel_params Selected sysctl values /proc/sys/
packages Installed package names and versions dpkg-query or rpm -qa
services Systemd unit names and states systemctl list-units
listening_ports TCP sockets in LISTEN state /proc/net/tcp
mac SELinux/AppArmor enforcement mode and policy /sys/fs/selinux, /sys/kernel/security/apparmor
firewall Packet-filter ruleset identity (SHA-256 + rule count) plus the parsed per-rule list for added/removed/reordered diff (rules embed IPs/ports — redacted by --redact-network) nft list ruleset, iptables-save
Opt-in collectors extend the snapshot further. Enable one with statedrift config enable (or enable all for everything); all are free and daemon-free, like everything else:
Collector What Drift it catches
cpu, kernel_counters CPU mode ticks; IP/TCP/UDP protocol counters Load and traffic anomalies (tracked as counters, never material drift)
processes Top-N processes by memory A resident process that wasn't there before
sockets Socket inventory per process A process quietly opening new connections
nic_drivers NIC driver and firmware versions (ethtool -i) A swapped driver or firmware image
filesystem Hash tree over configured roots (default /etc): per-file mode, ownership, size, SHA-256, plus a Merkle root_hash Per-file content, permission, or ownership changes
containers Running-container inventory from /proc cgroup membership — runtime-agnostic (Docker, containerd, CRI-O, podman) A container appearing (R37), disappearing (R38), or turning privileged (R39)
gpu NVIDIA GPU inventory from the driver's /proc interface GPU added/removed, driver or VBIOS drift (R40–R43)
dataplane SR-IOV VF counts and DPDK-bound NICs from /sys — devices handed to a userspace vfio-pci/uio driver, invisible to the kernel stack and its firewall A NIC silently rebound to userspace, VF counts changing (R44–R47)
harness Your AI agent's own config: Claude Code's settings.json, .mcp.json, and the user-scope ~/.claude.json that claude mcp add writes to A broadened tool permission, a new MCP server or hook, a model change (R49–R54)
filesystem growth is bounded by size and file-count caps; file paths and hashes are stored as-is (system config paths, nothing sensitive). The harness collector never stores secrets — MCP env values and embedded credentials are dropped at collect time, keeping only key names and a redacted fingerprint (details in the box below).
Your AI coding agent's own config is attack surface — statedrift version-controls it.
An agent's permissions, MCP servers, and hooks decide what it's allowed to touch. A silently broadened tool permission or a newly wired-in MCP server is a real privilege change, and nothing else is watching that file. The harness collector snapshots it, so the change shows up in the diff and trips a rule:
$ statedrift config enable harness # one-time opt-in (reads your ~/.claude — run as your own user) $ statedrift diff HEAD~1 HEAD --section harness harness.mcp: + ~/.claude.json filesystem: stdio # a new MCP server was wired in → R50
Secrets never enter the chain: MCP env values and credentials embedded in commands or URLs are dropped at collect time, leaving only env key names and a redacted SHA-256 fingerprint — so rotating a secret doesn't churn the snapshot, but changing the wiring does.
Each snapshot is SHA-256 hash-chained to the previous one. Modifying any snapshot breaks the chain — and statedrift verify catches it.
What statedrift does NOT do
Collect packet payloads or user content
Modify any system state
Require a cloud service or external dependency
Replace your monitoring/observability stack
Statedrift is an evidence tool, not a monitoring tool. It answers: "What was the state at time T, and can you prove it?"
Architecture
/proc, /sys, dpkg, systemctl, ip │ ▼ ┌──────────────────┐ │ Snapshot Collector│ Reads, normalizes, canonicalizes └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Hash Chain Engine│ canonical JSON → SHA-256 → prev_hash link └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Append-Only Store│ /var/lib/statedrift/chain/YYYY-MM-DD/HHMMSS.json └────────┬─────────┘ │ ┌────┬───┴───┬────┬──────┐ ▼ ▼ ▼ ▼ ▼ log show diff verify export │ ▼ Audit Bundle (.tar.gz + verify.sh)
Store layout
/var/lib/statedrift/ ├── head # SHA-256 of latest snapshot ├── baseline.json # Optional pinned compliance reference ├── chain/ │ ├── 2026-03-22/ │ │ ├── 140000.json # Snapshot at 14:00:00 UTC │ │ ├── 150000.json │ │ └── 160000.json │ └── 2026-03-23/ │ └── 090000.json └── exports/
Command reference
statedrift init
Initialize the snapshot store and take a genesis snapshot.
sudo statedrift init
Must be run once before any other command. Creates /var/lib/statedrift/ (or $STATEDRIFT_STORE).
statedrift snap
Take an on-demand snapshot.
sudo statedrift snap
Collects current host state, links it to the previous snapshot via hash chain, and writes it to the store. Prints a brief diff from the previous snapshot.
statedrift log
Show snapshot history.
statedrift log statedrift log --since 2026-03-01 statedrift log --since 2026-03-01 --until 2026-03-22
Flags:
Flag Description
--since YYYY-MM-DD Show snapshots on or after this date
--until YYYY-MM-DD Show snapshots on or before this date
--json Output as JSON array
statedrift show
Display the full contents of a specific snapshot.
statedrift show a3f8c1d2 # by hash prefix statedrift show HEAD # latest snapshot statedrift show HEAD~1 # one before latest statedrift show HEAD~3 # three before latest
Prints all sections: network, routes, kernel params, listening ports, packages (top 20), services.
Flags:
Flag Description
--json Output raw snapshot JSON
statedrift diff
Compare two snapshots.
statedrift diff HEAD~1 HEAD # last c
[truncated for AI cost control]