agentlens
Tamper-evident audit logging for Claude agents — Claude Code hooks and Anthropic SDK. Local-first, append-only, OSS.
Why
Anthropic logs API calls for their own safety monitoring — but that log is not yours. When your Claude-powered agent takes an action, you need your own tamper-evident record: for compliance (EU AI Act Art. 12, ISO/IEC 42001 A.6.2.8), incident response, and accountability.
agentlens captures every tool_use / tool_result event into a SHA-256 hash-chained JSONL file on your own machine — via Claude Code hooks (recommended) or as a drop-in Anthropic SDK wrapper. It can also block dangerous tool calls before they execute (deterministic rules, no LLM in the loop).
Quickstart: Claude Code / Claude Agent SDK (v0.6.0+)
pip install agentlens-io
agentlens hook install # prints the settings.json snippet
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{"matcher": "*", "hooks": [
{"type": "command", "command": "agentlens hook pre --log ~/.agentlens/audit.jsonl --block critical"}
]}
],
"PostToolUse": [
{"matcher": "*", "hooks": [
{"type": "command", "command": "agentlens hook post --log ~/.agentlens/audit.jsonl"}
]}
]
}
}
Now every tool call in Claude Code is audit-logged, and rm -rf /-class commands are denied before execution:
agentlens view ~/.agentlens/audit.jsonl # colorized event viewer
agentlens summary ~/.agentlens/audit.jsonl # per-session stats
agentlens verify ~/.agentlens/audit.jsonl # ✅ hash-chain integrity / ❌ tamper detected
agentlens feedback ~/.agentlens/audit.jsonl --emit-code # suggest whitelist rules from suppressed violations (v0.8.0+)
feedback reads the accumulated log — including the suppressed_violations that the whitelist keeps instead of deleting — and proposes narrowly-scoped WhitelistRules for rules with a high false-positive rate. It is suggestion-only: it never rewrites your ruleset. A ruleset that auto-tunes from its own logs can be poisoned, so a human stays in the loop. Flags: --min-occurrences N (default 3), --threshold F (default 0.9), --emit-code.
Options: --block critical|high|off (default critical), --whitelist rules.json (false-positive suppression — suppressed violations stay in the log), --standalone (post-hook logs tool_use+result when no pre-hook is registered). Hooks are fail-open: the logger can never break your agent loop.
Design principles
- Read-only interception — requests and responses are never altered
- Append-only writes — log entries cannot be edited after creation
- No AI in the logger — capture logic is deterministic code, not an LLM
- Your data stays local — FileWriter (default) writes to your own machine; no data leaves your environment
Usage: SDK wrapper
from agentlens import AuditedAnthropic
# Drop-in replacement for anthropic.Anthropic()
client = AuditedAnthropic(log_path="./audit.jsonl")
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=[...],
messages=[{"role": "user", "content": "..."}],
)
# Every tool_use and tool_result is now in audit.jsonl
Async (v0.7.0+)
AsyncAuditedAnthropic is the drop-in for anthropic.AsyncAnthropic — same
audit logging and pre-execution blocking, awaited:
from agentlens import AsyncAuditedAnthropic
client = AsyncAuditedAnthropic(log_path="./audit.jsonl", block_on_critical=True)
response = await client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=[...],
messages=[{"role": "user", "content": "..."}],
)
# Raises PreExecutionBlockedError before a critical tool call reaches you.
Streaming (v0.9.0+)
messages.stream() is wrapped too. Text passes through untouched; the audit
and the pre-execution gate fire when the message completes — before your code
reads the finished tool_use and acts on it. Works on the sync and async
clients:
with client.messages.stream(
model="claude-opus-4-6",
max_tokens=1024,
tools=[...],
messages=[{"role": "user", "content": "..."}],
) as stream:
for text in stream.text_stream:
print(text, end="")
message = stream.get_final_message() # tool_use audited + gated here
# block_on_critical raises PreExecutionBlockedError before you touch tool_use.
Provenance — who ran the agent (v0.10.0+)
The log answers what happened. Provenance adds who caused it and under what authority — stamped onto every event so a reader can prove attribution later.
from agentlens import AuditedAnthropic, Provenance
client = AuditedAnthropic(
log_path="./audit.jsonl",
provenance=Provenance(
agent_id="deploy-bot", # which agent
principal="alice@corp", # on whose behalf
authority=["repo:read", "ci:run"],# scopes it was granted
# run_id auto-generated; pass parent_run_id to record lineage
),
)
Hosted/CI agents usually get their identity from the platform via env, so
Provenance.from_env() reads AGENTLENS_AGENT_ID, AGENTLENS_PRINCIPAL,
AGENTLENS_AUTHORITY (comma/space separated), AGENTLENS_RUN_ID,
AGENTLENS_PARENT_RUN_ID. Provenance is covered by the hash chain, so tampering
with who did it breaks verify too. agentlens view shows a by: line per
call; summary breaks Tool Use down by agent. In v0.10 authority is recorded;
AuthorityPolicy (below) turns it into enforcement.
Authority enforcement — stay inside the granted scope (v0.11.0+)
v0.10 recorded the authority an agent was granted. AuthorityPolicy enforces
it: a tool whose required scopes aren't all present in the agent's granted
authority raises a critical violation, blocked by the same
block_on_critical gate as the danger rules. Record → enforce.
from agentlens import AuditedAnthropic, AuthorityPolicy, Provenance
client = AuditedAnthropic(
log_path="./audit.jsonl",
block_on_critical=True,
provenance=Provenance(agent_id="deploy-bot", authority=["repo:read"]),
authority_policy=AuthorityPolicy(
requirements={"bash": ["shell:exec"], "web_search": ["web:fetch"]},
# default_required=["*"] for deny-by-default once your scope map is complete
),
)
# deploy-bot was granted only repo:read → a bash tool_use is blocked before it runs.
Enforcement is opt-in (no policy → identical to v0.10, pure recording). A
tool is allowed iff every scope it requires is granted (AND semantics);
unmapped tools are allowed unless you set default_required. "*" in the
granted authority is a super-scope that allows everything.
Log format (JSONL)
{"event_type": "tool_use", "tool_use_id": "toolu_01xxx", "tool_name": "bash", "tool_input": {"command": "ls -la"}, "model": "claude-opus-4-6", "timestamp": "2026-04-05T10:00:00+00:00", "session_id": "...", "provenance": {"agent_id": "deploy-bot", "principal": "alice@corp", "authority": ["repo:read"], "run_id": "..."}}
{"event_type": "tool_result", "tool_use_id": "toolu_01xxx", "result_content": "file1.txt\nfile2.txt", "is_error": false, "timestamp": "2026-04-05T10:00:01+00:00", "session_id": "...", "provenance": {"agent_id": "deploy-bot", "run_id": "..."}}
Custom writer
from agentlens.writers import BaseWriter
class MyWriter(BaseWriter):
def write(self, event) -> None:
# send to your own DB, S3, SIEM, etc.
my_db.insert(event.to_json())
client = AuditedAnthropic(writer=MyWriter())
Run tests
pip install -e ".[dev]"
pytest tests/
License
MIT
Release files for agentlens-io 0.11.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agentlens_io-0.11.0.tar.gz | 41.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentlens_io-0.11.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.8 kB
Release files / agentlens_io-0.11.0.tar.gz
| Download URL | agentlens_io-0.11.0.tar.gz |
|---|---|
| Size | 41.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
350ef2e3b7b8c72f728f2fd05ae06adc9ff80b500cc57cbbba5ae783faac2ca3
|
|
BLAKE2b-256 checksum How to use checksums |
1216c4f117845b7c5a32249093d7155df3657e982c5cc6b6bbcef21bee1ee6bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / agentlens_io-0.11.0-py3-none-any.whl
| Download URL | agentlens_io-0.11.0-py3-none-any.whl |
|---|---|
| Size | 30.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ab05390beafcd84f1a3c9130e28789f02d0c1ddf947cb411992d9679e9075300
|
|
BLAKE2b-256 checksum How to use checksums |
f017ce90f93040925af4741e00a274e0198abd836b293ec84ac4dd7e738412f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|