@radware/claude-code-hooks
v0.1.0
Published
No-code Claude Code hooks that forward raw lifecycle/tool events to a Radware LLM Protection ingest endpoint.
Readme
@radware/claude-code-hooks
No-code Claude Code → Radware LLM Protection event forwarding. This package
installs Claude Code's native HTTP hooks so every lifecycle event POSTs its
raw JSON to your Radware ingest endpoint, with the API key in an Authorization
header. No scripts run per tool call, and nothing needs updating when Claude
changes its payload format — only the backend redeploys.
Quickstart
npx @radware/claude-code-hooks install --name "Your Name"
# you'll be prompted for your Radware API key (hidden input)npx downloads and runs the installer without leaving the npm package on your system.
The installer itself writes hook entries permanently into ~/.claude/settings.json —
they persist across sessions until you run uninstall.
Then launch Claude Code as usual. Set or rotate the key later with set-key; change
your name with set-name.
To point at a staging or local endpoint instead of production, pass --url:
npx @radware/claude-code-hooks install --name "Your Name" --url https://your-host/llmp/digester/claude-codeWhat it installs
Into ~/.claude/settings.json (user-global — captures all of your Claude Code
activity, every project):
- 5
httphook entries —SessionStart,UserPromptSubmit,PreToolUse,PostToolUse,Stop. Each POSTs the raw event to your URL withAuthorization: Bearer ${RADWARE_AGENTIC_PROTECTION_AGENT_KEY},X-Radware-User-Name: ${RADWARE_AGENTIC_PROTECTION_USER_NAME}, andtimeout: 3. env.RADWARE_AGENTIC_PROTECTION_AGENT_KEY— your key, andenv.RADWARE_AGENTIC_PROTECTION_USER_NAME— your name. Claude injects and interpolates both into the headers (declared in each hook'sallowedEnvVars). A name is required at install time: Claude Code sends no user identity, so this is how events are attributed in the Radware UI.
Commands
| Command | What it does |
|---|---|
| install [--url U] [--key K] [--name N] [--no-prompt] | register the hooks + store the key and name (name required) |
| set-key [--key K] | set/rotate the key (hidden prompt, or pipe via stdin) |
| set-name [--name N] | set/change the user name (prompt if omitted) |
| uninstall [--url U] [--purge] | remove our hooks (--purge also deletes the key + name) |
CLAUDE_CONFIG_DIR overrides which .claude directory is touched (used for testing).
Backend contract
Claude Code interprets a hook's HTTP response:
- empty
200→ clean no-op (fire-and-forget) - JSON body with
decision/permissionDecision→ blocks the tool call - plain-text body → injected into Claude's context
- non-2xx or unreachable → non-blocking error, the session continues
Radware LLM Protection uses this as follows:
- Monitoring (report-only) agents — the backend answers every event with an
empty
200and runs detection out of band. Strictly fire-and-forget; a down endpoint never breaks a session (it only drops events for the outage — there is no local buffer — and adds up totimeoutseconds per event if it hangs). - Blocking agents — the backend answers
PreToolUsesynchronously with apermissionDecision: "deny"body to stop a disallowed tool from running; all other events stay fire-and-forget. Because this round-trips before the tool runs, give blocking agents more headroom than the defaulttimeout: 3(Claude proceeds with the tool if the hook times out). TuneTIMEOUTinlib/config.js.
What's captured
Forwarded raw: session starts, user prompts, assistant message text, and tool
calls (input) + results (output). Every payload self-identifies via
hook_event_name; the backend parses and stitches by session_id (one Claude Code
session = one conversation).
The assistant's response text arrives on the Stop event via its
last_assistant_message field (the final assistant message of the turn) — no
transcript file reading and no per-tool scripts required. Intermediate assistant
messages and thinking are not captured; prompts, the final answer, and tool
activity are.
Payloads your backend receives
Each hook POSTs the raw Claude Code event JSON as the request body, unwrapped —
there is no envelope. The API key is in the Authorization: Bearer … header, not
in the body. Route on the hook_event_name field (present in every payload); the
backend does all parsing and stitching (e.g. grouping by session_id).
The shapes below reflect Claude Code's current version and are illustrative, not a frozen contract — Claude Code adds and renames fields across releases. Parse defensively: route on
hook_event_name, read the fields you need, ignore unknown ones. In particulartool_input/tool_responseare tool-specific and open-ended — store them as raw JSON rather than modeling one fixed schema.
Common fields (every event): session_id, transcript_path, cwd,
hook_event_name, and usually permission_mode
("default" | "plan" | "acceptEdits" | "auto" | "dontAsk" | "bypassPermissions").
1. SessionStart
{
"session_id": "abc123",
"transcript_path": "/home/you/.claude/projects/-home-you-myproj/abc123.jsonl",
"cwd": "/home/you/myproj",
"hook_event_name": "SessionStart",
"source": "startup",
"model": "claude-sonnet-4-6"
}Session beginning. source ∈ "startup" | "resume" | "clear" | "compact". model
present; agent_type / session_title optional.
2. UserPromptSubmit
{
"session_id": "abc123",
"transcript_path": "/home/you/.claude/projects/-home-you-myproj/abc123.jsonl",
"cwd": "/home/you/myproj",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "List the files here and summarize the README"
}prompt is the exact text the user submitted.
3. PreToolUse
{
"session_id": "abc123",
"transcript_path": "/home/you/.claude/projects/-home-you-myproj/abc123.jsonl",
"cwd": "/home/you/myproj",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "ls -la", "description": "List files" }
}Fires before a tool runs. tool_name + tool_input (shape varies by tool). This
is the one event a blocking backend can answer with permissionDecision: "deny".
4. PostToolUse
{
"session_id": "abc123",
"transcript_path": "/home/you/.claude/projects/-home-you-myproj/abc123.jsonl",
"cwd": "/home/you/myproj",
"permission_mode": "default",
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_input": { "command": "ls -la", "description": "List files" },
"tool_response": { "type": "text", "text": "total 8\ndrwxr-xr-x 4 you you ... README.md" }
}Fires after a tool runs — the one event that carries both the tool input and
its output (tool_response; some versions use tool_output).
5. Stop
{
"session_id": "abc123",
"transcript_path": "/home/you/.claude/projects/-home-you-myproj/abc123.jsonl",
"cwd": "/home/you/myproj",
"permission_mode": "default",
"hook_event_name": "Stop",
"stop_hook_active": false,
"last_assistant_message": "Here are the files, and a summary of the README ..."
}Marks the end of an assistant turn. Carries last_assistant_message — the full text
of the final assistant message — which is how the backend records Claude's answer
(no transcript parsing). May also carry effort or stop_hook_active.
tool_input / tool_response vary by tool
These are NOT a fixed schema — they mirror each tool's own arguments and result:
// Bash
"tool_input": { "command": "npm test", "description": "Run tests" }
// Edit
"tool_input": { "file_path": "/p/app.js", "old_string": "a", "new_string": "b" }
// Read
"tool_input": { "file_path": "/p/README.md" }
// MCP tools use namespaced names, e.g. tool_name: "mcp__memory__create_entities"tool_response likewise differs by tool — plain text output, a structured file
result, or { "type": "error", "text": "…" } on failure. Store it as opaque JSON
keyed by tool_name.
Security notes
- The key is stored as plaintext in
~/.claude/settings.jsonand is injected into every subprocess Claude spawns — that is the only way native HTTP hooks can read it. Use a write-only ingest key with a small blast radius. installbacks upsettings.jsontosettings.json.bakand merges idempotently.uninstallremoves only entries pointing at your ingest URL.
Configuration
The ingest URL defaults to the Radware production endpoint. Override it per-install with
--url or the RADWARE_AGENTIC_PROTECTION_INGEST_URL env var (useful for staging or local testing).
Development
node test/mock-ingest.mjs 8099 # stub endpoint that logs events and returns 200 empty
npm test # smoke test against a throwaway config dirEnd-to-end with real Claude Code: run the mock, then
CLAUDE_CONFIG_DIR=/tmp/cc-test node bin/cli.js install --url http://127.0.0.1:8099/ingest --key sk-test,
launch CLAUDE_CONFIG_DIR=/tmp/cc-test claude from any project, do something
tool-heavy, and watch the events land in the mock.
