Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 145 additions & 51 deletions claude-code/README.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,182 @@
# AgenTrust for Claude Code

Agent-integrity for the coding agent you already use. This plugin captures, every
session:
**Know your coding agent hasn't changed behind your back.**

- an **Agent Manifest** — what your Claude agent *is*: skills, tools, MCP servers,
model, permission policy, and instruction layer, each fingerprinted and signed
([agent-manifest](https://github.com/agentrust-io/agent-manifest)).
- a **TRACE Trust Record** — what your agent *did* this run, signed and
conformance-checkable ([trace](https://github.com/agentrust-io/trace-spec)).
Claude Code is not just a model. It is a model plus everything you have wired
around it: skills in `~/.claude/skills`, an allow/deny permission policy, MCP
servers, your `CLAUDE.md` and memory, and the tools it can call. That whole
composition decides what your agent can do to your machine and your code. Any of
it can change without you noticing.

and answers the one question that matters on a developer box:
This plugin answers one question at the start of every session:

> **Is the agent I'm running the one I approved nothing added, nothing subtracted?**
> **Is the agent I'm running the one I approved: nothing added, nothing subtracted?**

A rogue skill dropped into `~/.claude/skills`, a widened permission, an edited
`CLAUDE.md`, or an unexpected MCP server all change a fingerprint, and the next
SessionStart tells you.
## Why this matters

## What it does
You approved a setup you trust. Then, quietly, things drift:

| Surface | What runs | Needs crypto packages? |
|---|---|---|
| **SessionStart hook** | snapshot the agent from disk, diff against your approved baseline, warn in-session on drift | No (stdlib only) |
| `/manifest verify` | full diff including the live tool/MCP roster the agent reports | No |
| `/manifest approve` | make the current composition the approved baseline | No (add `--sign` for records) |
| `/trace` | build + sign the Agent Manifest and TRACE record, explain them in plain English | Yes |
- A skill you installed ships an update that now runs `curl` to an address you
never saw.
- A dependency's postinstall drops a `SKILL.md` into `~/.claude/skills`.
- A permission gets widened from `Bash(git:*)` to `Bash(*)` during some debugging
session and never gets narrowed back.
- An MCP server you added for one task is still connected weeks later.
- Your `CLAUDE.md` picks up an instruction you didn't write.

The hook is deliberately dependency-free so it never blocks session start. Signing
runs only when you ask for records.
None of these announce themselves. Each one changes what your agent will do on
your next run. This plugin fingerprints the whole composition, stores an approved
baseline, and tells you at session start the moment any of it moves. It is the
difference between "I think my agent is what I set up" and "I can prove it, and
I'd know within one session if it wasn't."

## Install
## Quickstart (about 60 seconds)

```bash
# 1. plugin (hooks + commands)
# 1. add the marketplace and install the plugin (hooks + commands)
/plugin marketplace add agentrust-io/integrations
/plugin install agentrust-claude-code
```

That's the whole install for drift detection. The SessionStart hook is
dependency-free (Python standard library only), so it never blocks a session.

On your **first** session after install, it records your baseline and tells you:

```
AgenTrust: baseline established for this Claude agent (7 skills, 2 MCP on disk).
Future sessions are checked against it. Run /manifest approve to re-baseline.
```

The baseline lives at `~/.claude/agentrust/baseline.json`. From then on, every
session is checked against it.

Signed records (`/trace` and `/manifest approve --sign`) are the only feature
that needs crypto packages. Install them when you want them:

# 2. only for signed records (/trace, /manifest approve --sign)
```bash
pip install -r claude-code/requirements.txt
```

First SessionStart establishes your baseline at `~/.claude/agentrust/baseline.json`.
Every later session is checked against it. Run `/manifest approve` whenever you
intentionally change your setup.
## The everyday loop

You mostly do nothing. You install it, and it stays quiet until something
changes. When it does, one line shows up at session start:

```
AgenTrust WARNING: 1 change(s) to your agent since baseline: added skill
pypi-helper. Run /manifest verify for detail, or /manifest approve to accept.
```

Two responses, both one command:

## What it captures (and what it does not)
**If the change is a surprise**, look at it. `/manifest verify` re-reads your
setup right now and lays out exactly what moved:

Captured, by fingerprint — never raw content, never secrets:
```
NOTHING ADDED, NOTHING SUBTRACTED? (vs approved baseline)
--------------------------------------------------------------
~ CHANGED permissions: policy_bundle
+ ADDED skill: exfil
>> 2 change(s) since baseline. Review above.
```

- **skills** — each `~/.claude/skills/*/SKILL.md`
- **permissions** — `~/.claude/settings.json` (the allow/deny policy)
- **instruction layer** — your `CLAUDE.md` / memory tree
- **tools + MCP servers** — the roster the agent reports at report time
- **model** — provider, id, version
Now you decide with the facts in front of you: remove the rogue skill, narrow the
permission, or accept it.

It never reads `~/.claude/.credentials.json`, and records skill / tool / MCP
**names** only, never tokens or environment values.
**If you made the change on purpose** (installed a skill you wanted, added an MCP
server for real work), tell the plugin this is the new normal:

## Known gaps (read before relying on it)
```
/manifest approve
```

- **Software-only, Level 0.** A normal dev box has no TEE, so the TRACE record is
software-only integrity, not hardware-rooted attestation. Labelled as such.
- **Instruction layer is a proxy.** The `system_prompt` fingerprint covers your
`CLAUDE.md` and memory, not Claude Code's internal system prompt, which is not
on disk.
- **`policy_language` mismatch.** agent-manifest's `policy_language` enum
(`cedar`/`rego`/`yaml-agt`/`composite`) has no value for host-native agent
permission systems like Claude Code's `settings.json`. Modelled as `composite`;
a spec value for host-native permissions is proposed upstream.
- **Hook visibility.** A shell hook cannot enumerate the live tool roster, so the
SessionStart check compares skills, permissions, and the instruction layer.
The full tool/MCP diff runs in `/manifest verify`, where the agent supplies the
live roster.
That promotes your current setup to the approved baseline. The warnings stop
until something moves again.

## Commands

| Command | What it does | Needs crypto packages? |
|---|---|---|
| SessionStart hook | Snapshot the agent from disk, diff against your approved baseline, warn in-session on drift | No |
| `/manifest verify` | Re-snapshot now and show the full diff, including the live tool and MCP roster the agent reports this session | No |
| `/manifest approve` | Make the current composition the approved baseline | No (add `--sign` for records) |
| `/manifest show` | Show the current composition without touching the baseline | No |
| `/trace` | Build and sign the Agent Manifest and TRACE record for this session, explained in plain English | Yes |

`/manifest verify` always re-reads your setup fresh, so it catches drift that
happens partway through a session, not just at startup.

## What it captures, and what it does not

It records **fingerprints, never raw content, never secrets:**

- **skills**: each `~/.claude/skills/*/SKILL.md`
- **permissions**: `~/.claude/settings.json` (the allow/deny policy)
- **instruction layer**: your `CLAUDE.md` and memory tree
- **tools and MCP servers**: the roster the agent reports, by name only
- **model**: provider, id, version

It never reads `~/.claude/.credentials.json`. It records skill, tool, and MCP
**names** only, never tokens, never environment values, never file contents.
A changed fingerprint tells you *that* something changed and *which category*,
which is what you need to go look.

## Signed records: Agent Manifest and TRACE

When you run `/trace` (or `/manifest approve --sign`), the plugin writes two
signed JSON records:

- an **Agent Manifest**: what your agent *is*, the full composition above, each
part fingerprinted and Ed25519-signed
([agent-manifest](https://github.com/agentrust-io/agent-manifest)).
- a **TRACE Trust Record**: what your agent *did* this run, signed and checkable
against the public conformance suite
([trace-spec](https://github.com/agentrust-io/trace-spec)).

These are shareable proof a third party can verify without trusting your machine.
Confirm a record with:

```bash
trace-tests verify --record trace.json --level 0
# or, if the console script is not on PATH:
python -m trace_tests.cli verify --record trace.json --level 0
```

## Known limits (read before relying on it)

This plugin is honest about what it is. On a normal developer machine:

- **Software-only, Level 0.** A dev box has no hardware TEE, so the TRACE record
is software integrity, not silicon-rooted attestation. It is labelled Level 0,
never presented as hardware-attested.
- **The instruction layer is a proxy.** The `system_prompt` fingerprint covers
your `CLAUDE.md` and memory tree, not Claude Code's internal system prompt,
which is not on disk.
- **`policy_language` is modelled as `composite`.** The agent-manifest
`policy_language` enum (`cedar` / `rego` / `yaml-agt` / `composite`) has no
value for host-native permission systems like Claude Code's `settings.json`.
A spec value for host-native permissions is proposed upstream.
- **The hook sees disk, commands see the session.** A shell hook cannot enumerate
the live tool roster, so the SessionStart check compares skills, permissions,
and the instruction layer. The full tool and MCP diff runs in `/manifest
verify`, where the agent supplies the live roster.

## Layout

```
claude-code/
.claude-plugin/plugin.json plugin manifest
hooks/hooks.json SessionStart -> engine/capture.py hook
commands/manifest.md /manifest capture | verify | approve
commands/manifest.md /manifest verify | approve | show
commands/trace.md /trace report
engine/capture.py capture engine (stdlib hook + signing report)
tests/test_capture.py stdlib-only tests
integration.yaml agentrust-io integration manifest
requirements.txt crypto deps for signing only
```

## License

Apache-2.0. Part of the [agentrust-io](https://github.com/agentrust-io)
open agent-governance toolchain.
42 changes: 27 additions & 15 deletions claude-code/commands/manifest.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
---
description: Capture, verify, or approve the integrity baseline of your Claude Code agent
description: Check, approve, or show the integrity baseline of your Claude Code agent
argument-hint: "[verify | approve | show]"
---

You are running the AgenTrust agent-integrity command. The engine is at
`${CLAUDE_PLUGIN_ROOT}/engine/capture.py`. It captures the agent's composition
(skills, tools, MCP servers, model, permissions, instruction layer) and diffs it
against the user's approved baseline at `~/.claude/agentrust/baseline.json`.
You are running the AgenTrust agent-integrity command. It answers one question
for the user: is the agent they are running the one they approved, with nothing
added and nothing subtracted since their baseline?

The engine is at `${CLAUDE_PLUGIN_ROOT}/engine/capture.py`. It captures the
agent's composition (skills, tools, MCP servers, model, permissions, instruction
layer) and diffs it against the approved baseline at
`~/.claude/agentrust/baseline.json`.

The shell hook cannot see the live tool roster, so YOU enrich it. Before running,
write the current session's real facts to a temp `live.json`:
write the current session's real facts to a temp `live.json`. Use real values you
actually observe this session, never invented ones:

```json
{
Expand All @@ -24,13 +29,20 @@ write the current session's real facts to a temp `live.json`:
Then dispatch on `$ARGUMENTS`:

- `verify` (default): run
`python "${CLAUDE_PLUGIN_ROOT}/engine/capture.py" verify --live-context live.json`
and show the "nothing added, nothing subtracted" result. Explain any change in
plain language and ask whether to approve it.
- `approve`: run `... approve --live-context live.json --sign --out .` to make the
current composition the new approved baseline and write signed records.
- `show`: run `... snapshot --live-context live.json` to display the current
composition without touching the baseline.
`python "${CLAUDE_PLUGIN_ROOT}/engine/capture.py" verify --live-context live.json`.
Verify always re-reads the setup fresh and merges your `live.json`, so it
reflects the agent's state right now, including drift introduced partway
through this session. Show the result and explain any change in plain language,
then ask whether to approve it.
- `approve`: run
`python "${CLAUDE_PLUGIN_ROOT}/engine/capture.py" approve --live-context live.json --sign --out .`
to make the current composition the new approved baseline and write signed
records. Use this when the user confirms the changes are intentional.
- `show`: run
`python "${CLAUDE_PLUGIN_ROOT}/engine/capture.py" snapshot --live-context live.json`
to display the current composition without touching the baseline.

Report the result in plain English. Never claim hardware attestation: on a normal
dev box this is software-only (Level 0) integrity, not silicon-rooted proof.
Report the result in plain English. Name the categories that changed (a skill, a
permission, an MCP server, the instruction layer) and what the user should do
about each. Never claim hardware attestation: on a normal dev box this is
software-only (Level 0) integrity, not silicon-rooted proof.
25 changes: 14 additions & 11 deletions claude-code/commands/trace.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@ session and explain it in plain English. Engine:

Steps:

1. Ensure the packages are installed (once):
`pip install agent-manifest agentrust-trace "agentrust-trace-tests>=0.2"`.
2. Write this session's real facts to `live.json` (do NOT invent values):
`model_id`, `model_provider`, `model_version`, `builtin_tools` (your actual
built-in tools), `mcp_servers` (the MCP servers actually connected now).
1. Ensure the signing packages are installed (once). Prefer the pinned set:
`pip install -r "${CLAUDE_PLUGIN_ROOT}/requirements.txt"`.
2. Write this session's real facts to `live.json`. Use values you actually
observe, never invented ones: `model_id`, `model_provider`, `model_version`,
`builtin_tools` (your actual built-in tools), `mcp_servers` (the MCP servers
actually connected now).
3. Run
`python "${CLAUDE_PLUGIN_ROOT}/engine/capture.py" report --live-context live.json --out .`
4. Optionally confirm the TRACE record passes the suite:
`trace-tests verify --record trace.json --level 0` (or
`python -m trace_tests.cli verify ...` if the script is not on PATH).
`python -m trace_tests.cli verify --record trace.json --level 0` if the
console script is not on PATH).

Then explain the report the user actually cares about:
- what the agent IS (skills, tools, MCP, model, permissions), each fingerprinted,
- what it DID this run (TRACE record, software-only / Level 0 on a dev box),

- what the agent IS: skills, tools, MCP, model, permissions, each fingerprinted.
- what it DID this run: the TRACE record, software-only and Level 0 on a dev box.
- whether anything changed since their approved baseline.

Be honest about scope: no TEE on a normal laptop means Level 0, not hardware
attestation. The "instruction layer" fingerprint covers CLAUDE.md and memory,
not Claude Code's internal system prompt (which is not on disk).
Be honest about scope. No TEE on a normal laptop means Level 0, not hardware
attestation. The instruction-layer fingerprint covers `CLAUDE.md` and memory, not
Claude Code's internal system prompt, which is not on disk.
8 changes: 7 additions & 1 deletion claude-code/engine/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,13 @@ def cmd_hook(args) -> int:

def cmd_verify(args) -> int:
base = _load(BASELINE)
snap = _load(LATEST) or snapshot(_live_from(args))
# Always re-snapshot so verify reflects the agent's CURRENT composition,
# not a cached session-latest.json. Otherwise drift introduced after
# session start (a skill dropped in, a widened permission) would be missed
# and verify would falsely report "nothing added, nothing subtracted".
# The live context supplied by the /manifest command is merged in here.
snap = snapshot(_live_from(args))
_save(LATEST, snap)
if base is None:
print("No approved baseline yet. Run /manifest approve to establish one.")
return 0
Expand Down
51 changes: 51 additions & 0 deletions claude-code/tests/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,54 @@ def test_snapshot_shape_from_real_home(tmp_path, monkeypatch):
assert key in snap
assert "tools" in snap["observed"] and "mcp" in snap["observed"]
assert snap["hashes"]["tool_catalog"].startswith("sha256:")


def _isolate_state(tmp_path, monkeypatch):
"""Point the engine's baseline/latest state at a temp dir."""
state = tmp_path / "agentrust"
monkeypatch.setattr(capture, "STATE_DIR", state)
monkeypatch.setattr(capture, "BASELINE", state / "baseline.json")
monkeypatch.setattr(capture, "LATEST", state / "session-latest.json")


class _Args:
live_context = None
out = "."
json = False
sign = False


def test_verify_detects_drift_introduced_after_baseline(tmp_path, monkeypatch, capsys):
"""verify must re-snapshot, not trust a stale session-latest.json.

Regression: verify used `_load(LATEST) or snapshot(...)`, so drift added
after session start (a rogue skill, a widened permission) was reported as
"nothing added, nothing subtracted" against the cached snapshot.
"""
_isolate_state(tmp_path, monkeypatch)

renderable = {
"captured_at": "2026-01-01T00:00:00Z",
"agent_id": "spiffe://claude-code.local/dev/box",
"model": {"provider": "anthropic", "model_id": "claude-x", "version": "1"},
"allow_rules": [],
"hashes": {
"system_prompt": "sha256:" + "1" * 64, "policy_bundle": "sha256:" + "2" * 64,
"skills_set": "sha256:" + "3" * 64, "tool_catalog": "sha256:" + "4" * 64,
},
}
clean = _base(skills={"deploy": "sha256:" + "a" * 64}, **renderable)
capture._save(capture.BASELINE, clean)
# A stale latest from an earlier, clean point in the session.
capture._save(capture.LATEST, clean)

# The agent has since drifted: a rogue skill appeared on disk.
drifted = _base(skills={"deploy": "sha256:" + "a" * 64, "exfil": "sha256:" + "b" * 64}, **renderable)
monkeypatch.setattr(capture, "snapshot", lambda live=None: drifted)

assert capture.cmd_verify(_Args()) == 0
out = capsys.readouterr().out
assert "ADDED skill: exfil" in out
assert "1 change(s) since baseline" in out
# the success line must NOT appear when drift is present
assert "Verified: nothing added, nothing subtracted" not in out