A dashcam for your AI agents: local, tamper-evident recorder for process/file/network activity, with a LangChain/LangGraph drop-in prompt-injection circuit breaker.
Project description
Handcuff
A dashcam for your AI agents. Handcuff records the OS-level side effects of an AI agent — processes spawned, files written, network connections opened — into a local, tamper-evident, hash-chained SQLite timeline you can replay, search, export, and verify. Real-time policy alerts flag risky behavior (sudo, mass deletes, writes outside the workspace, unknown domains, force pushes) while the agent runs.
Local-only by design: nothing ever leaves your machine except an optional, user-configured, content-light webhook (HTTPS required). This is enforced by a test_no_egress release-gate test.
Platform note: the current implementation targets Windows (psutil + watchdog capture, Windows ACL/Firewall enforcement, PyInstaller binary). The original planning docs describe a Linux-first design; that pivot is deliberate. Most capture code is psutil-generic, so Linux/macOS support is feasible but currently untested.
Install
# From PyPI (Python 3.11+) — note the distribution name; the import
# name and CLI command are still `handcuff` either way.
pip install handcuff
# with the LangChain/LangGraph integration:
pip install "handcuff[langchain]"
# From source, for development
pip install .
# Or use the prebuilt binary
dist\handcuff.exe --help
Quickstart
# Record an agent (or any command) and everything it does
handcuff watch -- python my_agent.py
# List recorded sessions, then open the live dashboard
handcuff sessions
handcuff tui
# Replay a past session, jumping between alerts
handcuff replay <SESSION_ID>
# Export a report (json | md | html) and verify its hash chain
handcuff export <SESSION_ID> --format html
handcuff verify <SESSION_ID>
# Attach to something already running
handcuff attach <PID>
# Environment diagnostics
handcuff doctor
Key features
-
Tamper-evident timeline — every event is SHA-256 hash-chained; session heads are Ed25519-signed.
handcuff verifyre-validates the chain from the DB or from a JSON export. -
Capture — process spawn/exit (psutil), file write/delete/rename (watchdog), network connects (psutil). Optional
--hash-contentrecords a SHA-256 of written files (≤1 MiB; hash only, never content). -
Rules & alerts — safe expression DSL (AST-evaluated, no
eval), windowed rules, default ruleset, trust allowlist (handcuff rules trust add domain example.com), optional webhook on critical alerts. -
Redaction — secrets (API keys, tokens) are scrubbed before events are persisted.
-
Enforcement (opt-in, Windows) —
watch --enforceapplies real OS-level blocking: Deny-ACLs on sensitive files (no admin needed) and, if elevated, a Windows Firewall outbound block with--allow-domainexceptions. Always reverted on exit. -
Cooperative gateway + prompt-injection circuit breaker —
handcuff.gateway.Gatewayis a validating proxy for agent frameworks that route their file/network tool calls through it (same trust model as an OpenAI/LangChain/MCP tool sandbox — see its docstring for exact scope, it cannot stop raw shell/socket access). Every fetched URL (and any file read markeduntrusted=True) is scanned with a heuristic prompt-injection signature scanner (handcuff.rules.injection) covering instruction-override phrasing, fake system/assistant turn markers, jailbreak personas, data-exfiltration instructions, and obfuscation tells (hidden Unicode, suspicious base64 blobs). A match "taints" the session for a configurable window and the gateway then blocks further network egress — the deterministic mitigation for Simon Willison's "lethal trifecta" (private data access + untrusted content + external communication), rather than trying to out-guess the injection semantically. This is a regex/heuristic tripwire, not a semantic guarantee — a novel or paraphrased injection can still slip past it; treat a miss as "not a known pattern" rather than "safe."from pathlib import Path from handcuff.gateway.gateway import Gateway, GatewayDenied with Gateway(workspace_root=Path("./agent_workspace")) as gw: page = gw.http_request("https://example.com/some-doc") # scanned automatically if gw.is_tainted: print("suspected prompt injection:", gw.taint_info.reason) try: gw.http_request("https://wherever-the-doc-said-to-send-data.example") except GatewayDenied as e: print("blocked:", e) # circuit breaker fired
-
TUI dashboard — trust meter, flagged-event cards, live monitoring, recommendation panel (Textual).
-
Exports — JSON (with embedded verification block), Markdown, and self-contained HTML reports.
-
Multi-agent framework integration (LangChain / LangGraph), one line of code —
handcuff.integrations.langchain.HandcuffHandleris a reallangchain-coreBaseCallbackHandler. Pass it ascallbacks=[handler]to any chain, agent, or graph and it records every chain/tool/retriever call as a hash-chained event and runs the same injection scanner + taint circuit breaker as the Gateway, scoped per agent. It works for concurrent/parallel agents (multiple LangGraph nodes,ThreadPoolExecutor,asyncio.gather) because it keys state off LangChain's ownrun_id/parent_run_idrun tree — the same mechanism LangChain uses internally to keep parallel runs from crossing wires — with all shared state protected by a lock for real cross-thread safety.pip install handcuff[langchain]to pull inlangchain-core. Verified against the real package end-to-end (parallelThreadPoolExecutoragents, real@tools, real blocking) — seetests/test_langchain_real.py.from handcuff.integrations.langchain import HandcuffHandler handler = HandcuffHandler() # writes to the same DB `handcuff tui` watches result = my_graph.invoke(inputs, config={"callbacks": [handler]}) handler.close()
Run
handcuff tuiin another terminal while your agent(s) run — it polls the same DB and picks the session up live automatically, no extra wiring. Honest scope: this only sees what happens through LangChain's own abstractions (chains/tools/retrievers) — a tool implementation that shells out or callsrequestsdirectly with no LangChain wrapper around it never fires these callbacks, and taint is scoped to one run tree (one top-level.invoke()), not the whole process.
Configuration
handcuff config list
handcuff config set retention_days 14
handcuff config set webhook_url https://hooks.example.com/handcuff
Config lives at ~/.config/handcuff/config.yaml; the DB at ~/.local/share/handcuff/handcuff.db; the signing key at ~/.config/handcuff/ed25519.key.
Development
python -m venv .venv && .venv\Scripts\pip install -e .[dev,langchain]
.venv\Scripts\python -m pytest tests -q # ~180 tests
.venv\Scripts\ruff check handcuff tests
scripts\build_windows.ps1 # PyInstaller binary
Security release gates (must always pass): no-egress, chain-tamper detection, redaction, DSL safety, watchdog reconcile.
Documentation
Planning and design docs live in the repo root: PRD, TRD, Security, Implementation Plan, Tickets, and the TUI spec (Handcuff_SPEC_Part1-4.md).
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 handcuff-0.2.0.tar.gz.
File metadata
- Download URL: handcuff-0.2.0.tar.gz
- Upload date:
- Size: 73.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37adfe74932f2ab5cd5d0f94224830c1c3c5371b804251469aaaa06009736c65
|
|
| MD5 |
6c971dcf41741bd1db202656a8e5557a
|
|
| BLAKE2b-256 |
fa2dcf599fc2d17ca2366f6fb44b657a0988ed3bfff83808462b7ea0fb360a11
|
File details
Details for the file handcuff-0.2.0-py3-none-any.whl.
File metadata
- Download URL: handcuff-0.2.0-py3-none-any.whl
- Upload date:
- Size: 97.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3690b508d143eb2c1dc0fd93697483694f6aaf71e1527bb6eb580ad235be7a1d
|
|
| MD5 |
38864a7b9fcdb8a344d536fdfcf1249b
|
|
| BLAKE2b-256 |
f699b9d6b734e2bac4e130ecde4a4cb679c00332898609042a5089f1988d5123
|