Configure Claude Code Hooks to POST events to the ReCursor bridge server for mobile consumption.
Claude Code provides a Hooks system that allows plugins to observe and react to events. ReCursor uses this system to receive real-time events from Claude Code, enabling the mobile app to display agent activity with OpenCode-style UI components.
Important: Hooks are one-way observation only. They cannot inject messages or control the Claude Code session. For bidirectional communication, use the Agent SDK for parallel sessions.
Based on the Claude Code hooks system source truth, the following events are confirmed:
| Event | Trigger | Payload |
|---|---|---|
SessionStart |
New Claude Code session begins | Session metadata |
SessionEnd |
Session terminates | Session summary |
PreToolUse |
Agent about to use a tool | Tool, params, risk level |
PostToolUse |
Tool execution completed | Tool, result, metadata |
UserPromptSubmit |
User submits a prompt | Prompt text, context |
Stop |
Agent stops execution | Stop reason, context |
SubagentStop |
Subagent stops execution | Subagent result, context |
PreCompact |
Before context compaction | Context stats |
Notification |
System notification | Message, level |
Note: Other events may exist but are not confirmed in the current Claude Code hooks implementation.
Hooks are configured via a hooks.json file in your Claude Code plugin directory. Claude Code supports two hook types:
type: "command"— Execute bash commands for deterministic checkstype: "prompt"— Use LLM-driven decision making for context-aware validation
Create a hooks.json file in your plugin's hooks/ directory:
{
"description": "ReCursor bridge integration - forward events to mobile app",
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "curl -X POST https://100.78.42.15:3000/hooks/event -H 'Content-Type: application/json' -H 'Authorization: Bearer your-bridge-token' -d @-",
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "curl -X POST https://100.78.42.15:3000/hooks/event -H 'Content-Type: application/json' -H 'Authorization: Bearer your-bridge-token' -d @-",
"timeout": 10
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "curl -X POST https://100.78.42.15:3000/hooks/event -H 'Content-Type: application/json' -H 'Authorization: Bearer your-bridge-token' -d @-",
"timeout": 10
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "curl -X POST https://100.78.42.15:3000/hooks/event -H 'Content-Type: application/json' -H 'Authorization: Bearer your-bridge-token' -d @-",
"timeout": 10
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "curl -X POST https://100.78.42.15:3000/hooks/event -H 'Content-Type: application/json' -H 'Authorization: Bearer your-bridge-token' -d @-",
"timeout": 10
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "curl -X POST https://100.78.42.15:3000/hooks/event -H 'Content-Type: application/json' -H 'Authorization: Bearer your-bridge-token' -d @-",
"timeout": 10
}
]
}
]
}
}For context-aware validation, use prompt-based hooks:
{
"description": "Validation hooks with LLM evaluation",
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "Validate file write safety. Check: system paths, credentials, path traversal, sensitive content. Return 'approve' or 'deny'.",
"timeout": 30
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate if the task is truly complete. Check: all requirements met, tests passing, documentation updated. Return 'complete' or 'continue'.",
"timeout": 30
}
]
}
]
}
}~/.claude-code/plugins/
└── recursor-bridge/
├── hooks.json # Hook definitions
└── README.md # Plugin documentation
The ReCursor bridge exposes a /hooks/event endpoint to receive Claude Code events:
import express from 'express';
import { EventEmitter } from 'events';
const app = express();
const eventBus = new EventEmitter();
// Middleware
app.use(express.json());
// Hook event endpoint
app.post('/hooks/event', validateHookToken, (req, res) => {
const hookEvent = req.body;
if (!validateHookEvent(hookEvent)) {
return res.status(400).json({ error: 'Invalid hook event' });
}
// Emit for internal handling
eventBus.emit('claude-event', hookEvent);
// Queue for offline mobile clients
eventQueue.enqueue(hookEvent);
res.status(200).json({ received: true });
});
// Token validation middleware
function validateHookToken(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader?.replace('Bearer ', '');
if (token !== process.env.HOOK_TOKEN) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// Event validation
function validateHookEvent(event: unknown): event is HookEvent {
return (
typeof event === 'object' &&
event !== null &&
'event_type' in event &&
typeof (event as HookEvent).event_type === 'string'
);
}
// Broadcast to connected mobile clients
function broadcastToMobile(event: HookEvent) {
mobileClients.forEach(client => {
if (client.sessionId === event.session_id) {
client.ws.send(JSON.stringify({
type: 'claude_event',
payload: event
}));
}
});
}
eventBus.on('claude-event', broadcastToMobile);{
"event_type": "PostToolUse",
"session_id": "sess-abc123",
"timestamp": "2026-03-17T10:32:00Z",
"payload": {
"tool": "edit_file",
"params": {
"file_path": "/home/user/project/lib/main.dart",
"old_string": "void main() {",
"new_string": "void main() async {"
},
"result": {
"success": true,
"diff": "... unified diff ..."
},
"metadata": {
"token_count": 150,
"duration_ms": 250
}
}
}{
"event_type": "PreToolUse",
"session_id": "sess-abc123",
"timestamp": "2026-03-17T10:32:00Z",
"payload": {
"tool": "Bash",
"params": {
"command": "rm -rf /important",
"description": "Clean up files"
},
"risk_level": "high",
"requires_approval": true
}
}{
"event_type": "SessionStart",
"session_id": "sess-abc123",
"timestamp": "2026-03-17T10:30:00Z",
"payload": {
"working_directory": "/home/user/project",
"initial_prompt": "Refactor the authentication module",
"environment": {
"shell": "zsh",
"claude_version": "2.1.0"
}
}
}{
"event_type": "SessionEnd",
"session_id": "sess-abc123",
"timestamp": "2026-03-17T11:45:00Z",
"payload": {
"duration_seconds": 4500,
"tools_used": ["Read", "Edit", "Bash"],
"summary": "Completed authentication refactoring",
"exit_code": 0
}
}{
"event_type": "Stop",
"session_id": "sess-abc123",
"timestamp": "2026-03-17T11:45:00Z",
"payload": {
"reason": "task_completed",
"message": "Task completed successfully",
"context": {
"files_modified": 3,
"tests_passed": true
}
}
}{
"event_type": "UserPromptSubmit",
"session_id": "sess-abc123",
"timestamp": "2026-03-17T10:35:00Z",
"payload": {
"prompt": "Add error handling to the login function",
"context": {
"current_file": "lib/auth.dart",
"cursor_position": 145
}
}
}- Token Authentication: Always use the
Authorization: Bearer <token>header - HTTPS Only: Use TLS for all hook communications in production
- IP Allowlisting: Restrict bridge endpoint to known Claude Code IPs if possible
- Payload Validation: Validate all incoming hook events before processing
- Rate Limiting: Implement rate limiting on the
/hooks/eventendpoint
- Verify
hooks.jsonsyntax is valid JSON - Check plugin is in correct directory:
~/.claude-code/plugins/ - Ensure hook commands have execute permissions
- Review Claude Code logs for hook execution errors
- Verify bridge URL is accessible from Claude Code host
- Check firewall rules allow outbound HTTP to bridge
- Confirm authentication token matches
- Test with simple
curlcommand manually
- Ensure events match expected schema
- Check
event_typeis in the confirmed events list - Verify
timestampis ISO 8601 format - Confirm
session_idis present and valid
- Claude Code Hook Development Guide
- Hookify Plugin Example
- Agent SDK Integration — For bidirectional communication
- Bridge Protocol — WebSocket message specification
Last updated: 2026-03-17 | Verified against Claude Code source truth