Skip to main content

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 verify re-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-content records 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 --enforce applies real OS-level blocking: Deny-ACLs on sensitive files (no admin needed) and, if elevated, a Windows Firewall outbound block with --allow-domain exceptions. Always reverted on exit.

  • Cooperative gateway + prompt-injection circuit breakerhandcuff.gateway.Gateway is 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 marked untrusted=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 codehandcuff.integrations.langchain.HandcuffHandler is a real langchain-core BaseCallbackHandler. Pass it as callbacks=[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 own run_id/parent_run_id run 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 in langchain-core. Verified against the real package end-to-end (parallel ThreadPoolExecutor agents, real @tools, real blocking) — see tests/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 tui in 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 calls requests directly 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

handcuff-0.2.0.tar.gz (73.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

handcuff-0.2.0-py3-none-any.whl (97.8 kB view details)

Uploaded Python 3

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

Hashes for handcuff-0.2.0.tar.gz
Algorithm Hash digest
SHA256 37adfe74932f2ab5cd5d0f94224830c1c3c5371b804251469aaaa06009736c65
MD5 6c971dcf41741bd1db202656a8e5557a
BLAKE2b-256 fa2dcf599fc2d17ca2366f6fb44b657a0988ed3bfff83808462b7ea0fb360a11

See more details on using hashes here.

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

Hashes for handcuff-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3690b508d143eb2c1dc0fd93697483694f6aaf71e1527bb6eb580ad235be7a1d
MD5 38864a7b9fcdb8a344d536fdfcf1249b
BLAKE2b-256 f699b9d6b734e2bac4e130ecde4a4cb679c00332898609042a5089f1988d5123

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page