Chock.sh Build your plugin

Agentseam

Use agentseam without chock

Install agentseam, write a handler, wire it into your agents' hooks and see it refuse, from a recorded run.

A handler is a Python function that takes an Event and returns a Decision. Agentseam wires the command that runs it into each agent's hook config. Every output on this page is from a real run, recorded with the agentseam release this site documents.

Install

Agentseam is one Python package with no runtime dependencies and one console script, agentseam. Installing chock installs it too, at the commit below. Agentseam's README installs it from PyPI with pip install agentseam. A PyPI release can differ from this commit. To get the code these pages document, install from the commit (ask the user before running it):

pip install "agentseam @ git+https://github.com/open-coder-ai/agentseam@17dd700eb0b1f8af9c6bfd42bd9438fd5e4445cb"

It needs Python >=3.9.

The files

The run below starts in an empty git repository holding these files. my_handler.py is the handler from agentseam's README Quick start, word for word. The site's tests check it against agentseam's docs/quickstart.sh at the pinned commit. The two payloads take the shape of the Claude Code PreToolUse example in agentseam's examples/generated/claude_code.md, with their content changed. examples/generated/claude_code.md

my_handler.py

The handler. run(handler) reads one payload from stdin, identifies the agent that sent it (best effort, with adapters.detect), calls the handler with the normalized Event, and prints the reply in that agent's dialect.

from agentseam import run, Decision

def handler(event):
    if event.event == "pre_tool" and "AKIA" in (event.content or ""):
        return Decision.deny("no AWS keys in memory files")
    return Decision.allow()

run(handler)

deny.json

A Claude Code PreToolUse payload: the agent wants to write a line with AKIA in it to AGENTS.md.

{"cwd": "/repo", "hook_event_name": "PreToolUse", "permission_mode": "default", "session_id": "example", "tool_input": {"content": "aws_access_key_id = AKIA-PLACEHOLDER", "file_path": "AGENTS.md"}, "tool_name": "Write", "transcript_path": "/repo/.claude/transcript.jsonl"}

allow.json

The same payload with harmless content.

{"cwd": "/repo", "hook_event_name": "PreToolUse", "permission_mode": "default", "session_id": "example", "tool_input": {"content": "Use ruff for lint.", "file_path": "AGENTS.md"}, "tool_name": "Write", "transcript_path": "/repo/.claude/transcript.jsonl"}

in_process.py

The same check called in process with handle(), which returns the reply instead of printing it.

import json

from agentseam import Decision, handle


def handler(event):
    if event.event == "pre_tool" and "AKIA" in (event.content or ""):
        return Decision.deny("no AWS keys in memory files")
    return Decision.allow()


with open("deny.json", encoding="utf-8") as payload:
    raw = json.load(payload)
text, code, event, decision = handle(raw, handler, "claude_code")
print("event:", event.agent, event.event, event.tool, event.path)
print("decision:", decision.outcome, decision.reason)
print("exit code:", code)
print("printed:", text)

The recorded run

Real runs, recorded by python -m sitegen.agentseamdoc run --record with agentseam at the pinned commit. The steps of data/agentseam-run.yaml ran in order, offline, in one fresh git repository at /work/demo (made with git init -b main), with HOME at /home/demo. Those paths stand for the throwaway folders the runs used. Output is stdout and stderr together. CI re-runs the steps and fails when any output or exit code differs.

Step 1 of 9, exit code 0. The version of the installed agentseam.

$ agentseam --version
0.4.1

Step 2 of 9, exit code 0. Every agent in agentseam's matrix: its id, tier and hook config file. install takes these ids, or all.

$ agentseam agents
aider            none           (no hook config)
amp              unadapted      (no hook config)
antigravity      block+rewrite  .agents/hooks.json
claude_code      block+rewrite  .claude/settings.json
cline            block          .clinerules/hooks
codex_cli        block+rewrite  .codex/hooks.json
copilot          unadapted      (no hook config)
cursor           block+rewrite  .cursor/hooks.json
deepseek_harness unadapted      (no hook config)
devin            block+rewrite  .devin/hooks.v1.json
factory_droid    block          .factory/hooks.json
gemini_cli       block+rewrite  .gemini/settings.json
goose            block          .agents/plugins/agentseam/hooks/hooks.json
grok             block          .grok/hooks/agentseam.json
junie            block+rewrite  ~/.junie/config.json
kilo_code        unadapted      (no hook config)
kimi_code        block          ~/.kimi-code/config.toml
kiro             unadapted      (no hook config)
mistral_vibe     block+rewrite  .vibe/hooks.toml
opencode         unadapted      (no hook config)
openhands        block          .openhands/hooks.json
replit           unadapted      (no hook config)
roo_code         unadapted      (no hook config)
tabnine          block+rewrite  .tabnine/agent/settings.json
vscode_copilot   block+rewrite  .github/hooks/*.json
windsurf         block          .devin/hooks.json
zed              none           (no hook config)

Step 3 of 9, exit code 0. The README's install line. It wires python3 my_handler.py at pre_tool into each agent that has a hook adapter, and prints each agent's grade there. Junie's and Kimi Code's config files sit under HOME.

$ agentseam install all 'python3 my_handler.py' --events pre_tool --repo .
wired antigravity      -> .agents/hooks.json   [pre_tool=best-effort]
wired claude_code      -> .claude/settings.json   [pre_tool=best-effort]
wired cline            -> .clinerules/hooks   [pre_tool=best-effort]
                       not live until trusted: in the VS Code extension, tick Enable Hooks under Cline's Feature settings; hooks run on macOS and Linux only
wired codex_cli        -> .codex/hooks.json   [pre_tool=best-effort]
                       not live until trusted: run /hooks in Codex and trust the new hook (trust is per hook hash: re-trust after any change)
wired cursor           -> .cursor/hooks.json   [pre_tool=enforceable]
wired devin            -> .devin/hooks.v1.json   [pre_tool=best-effort]
wired factory_droid    -> .factory/hooks.json   [pre_tool=best-effort]
wired gemini_cli       -> .gemini/settings.json   [pre_tool=best-effort]
wired goose            -> .agents/plugins/agentseam/hooks/hooks.json   [pre_tool=best-effort]
wired grok             -> .grok/hooks/agentseam.json   [pre_tool=best-effort]
                       not live until trusted: trust it in the agent
wired junie            -> /home/demo/.junie/config.json   [pre_tool=best-effort]
wired kimi_code        -> /home/demo/.kimi-code/config.toml   [pre_tool=best-effort]
wired mistral_vibe     -> .vibe/hooks.toml   [pre_tool=best-effort]
                       not live until trusted: trust it in the agent
wired openhands        -> .openhands/hooks.json   [pre_tool=best-effort]
wired tabnine          -> .tabnine/agent/settings.json   [pre_tool=best-effort]
wired vscode_copilot   -> .github/hooks/agentseam.json   [pre_tool=best-effort]
wired windsurf         -> .devin/hooks.json   [pre_tool=best-effort]

Step 4 of 9, exit code 0. What it wrote for Claude Code. Agentseam marks each entry _agentseam, so uninstall removes only its own entries.

$ cat .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 my_handler.py claude_code",
            "_agentseam": "agentseam"
          }
        ],
        "_agentseam": "agentseam"
      }
    ]
  }
}

Step 5 of 9, exit code 0. The hook as Claude Code runs it, fed the payload with AKIA. The handler returned Decision.deny, and agentseam printed the refusal in Claude Code's PreToolUse dialect.

$ python3 my_handler.py < deny.json
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "no AWS keys in memory files"}}

Step 6 of 9, exit code 0. The harmless payload. The handler returned Decision.allow(), and agentseam printed nothing: the exit code carries the answer.

$ python3 my_handler.py < allow.json

Step 7 of 9, exit code 0. In process, handle() returns the normalized Event, the Decision, the exit code and the text run() would print.

$ python3 in_process.py
event: claude_code pre_tool Write AGENTS.md
decision: deny no AWS keys in memory files
exit code: 0
printed: {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "no AWS keys in memory files"}}

Step 8 of 9, exit code 0. uninstall all removes agentseam's entries from every agent's config.

$ agentseam uninstall all --repo .
antigravity      removed
claude_code      removed
cline            removed
codex_cli        removed
cursor           removed
devin            removed
factory_droid    removed
gemini_cli       removed
goose            removed
grok             removed
junie            removed
kimi_code        removed
mistral_vibe     removed
openhands        removed
tabnine          removed
vscode_copilot   removed
windsurf         removed

Step 9 of 9, exit code 0. Claude Code's settings file after uninstall.

$ cat .claude/settings.json
{}

Built from engine 5a4b6287a46e and catalog 1e60d211c1bb.