Here's a practical implementation of an observability plugin/hook for your Claude Code and Cursor workflows. I'll provide:
-
Python version for Claude Code: A hook script that integrates with OpenTelemetry (OTel) to trace and log lifecycle events (e.g., tool uses, prompts). This can be configured in
.claude/settings.jsonfor multiple hooks. It exports traces to a console exporter for simplicity (you can swap to Jaeger, Zipkin, or any OTel-compatible backend like Langfuse or Arize by updating the exporter). -
TypeScript version for Cursor: A VS Code extension (since Cursor is VS Code-based) that logs AI-powered suggestions and code changes. It listens to text document changes and completion acceptance events, then logs them (to console or a file). This enhances observability for Cursor's AI features like autocomplete and Composer. Install via VS Code marketplace or sideload.
Both focus on basic observability: logging events, timings, and details for debugging/monitoring. Extend with metrics (e.g., token usage) or integrations (e.g., send to Datadog/Sentry).
This is a single Python script (observability_hook.py) that handles any hook event. It uses OTel to create spans for events, capturing details like tool names, inputs, and durations. Install dependencies: pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp.
#!/usr/bin/env python
import sys
import json
import time
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.trace import SpanKind
# Initialize OTel (run once per session or use global init)
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) # Export to console; swap for OTLP
tracer = trace.get_tracer("claude_code_observability")
def main():
start_time = time.time()
try:
data = json.load(sys.stdin)
event_name = data.get("hook_event_name", "Unknown")
with tracer.start_as_current_span(
f"ClaudeHook:{event_name}",
kind=SpanKind.INTERNAL,
attributes={
"session_id": data.get("session_id"),
"tool_name": data.get("tool_name"),
"prompt": data.get("prompt"),
"tool_input": json.dumps(data.get("tool_input", {})),
}
) as span:
# Custom logic per event (e.g., block or modify)
if event_name == "PreToolUse" and data.get("tool_name") == "Bash":
cmd = data.get("tool_input", {}).get("command", "")
if "rm" in cmd:
print(json.dumps({"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "Blocked rm"}}))
sys.exit(2)
# Add event duration
duration = time.time() - start_time
span.add_event("HookProcessed", attributes={"duration_ms": duration * 1000})
# Optional: Output additional context for Claude
print(json.dumps({"additionalContext": f"Observed {event_name} event"}))
sys.exit(0)
except Exception as e:
print(json.dumps({"error": str(e)}), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()- Save as
.claude/hooks/observability_hook.py(make executable:chmod +x). - Configure in
.claude/settings.json(or via/hooksmenu):{ "hooks": { "PreToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "uv run .claude/hooks/observability_hook.py"}]}], "PostToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "uv run .claude/hooks/observability_hook.py"}]}], "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "uv run .claude/hooks/observability_hook.py"}]}] } } - Run Claude Code; events will log traces like:
{ "name": "ClaudeHook:PreToolUse", "context": {"trace_id": "...", "span_id": "..."}, "attributes": {"tool_name": "Bash", ...}, "events": [{"name": "HookProcessed", "attributes": {"duration_ms": 12.34}}] }- For production, replace
ConsoleSpanExporterwithOTLPSpanExporter(e.g., to Langfuse: set endpoint and headers).
- For production, replace
This gives under-the-hood visibility into agent lifecycles, bottlenecks, and errors.
This is a basic VS Code extension (cursor-observability) that logs AI suggestions and code changes. It uses VS Code API to:
- Listen for text changes (e.g., AI insertions).
- Hook into completion acceptance (via
onDidAcceptCompletionItemif available, or proxy via disposables). - Log to an output channel or file.
Cursor's AI (e.g., Tab autocomplete) triggers these events. Install: Package as VSIX or publish to marketplace.
{
"name": "cursor-observability",
"displayName": "Cursor Observability",
"description": "Logs AI suggestions and code changes for observability",
"version": "0.1.0",
"engines": {"vscode": "^1.85.0"},
"categories": ["Other"],
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js",
"contributes": {
"commands": [{"command": "cursor-observability.viewLogs", "title": "View Observability Logs"}]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.85.0",
"@types/node": "20.x",
"typescript": "^5.3.3"
}
}import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
let outputChannel: vscode.OutputChannel;
let logFile: string;
export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('Cursor Observability');
logFile = path.join(context.extensionPath, 'observability.log');
// Log text changes (e.g., AI insertions)
const changeDisposable = vscode.workspace.onDidChangeTextDocument(event => {
const changes = event.contentChanges.map(change => ({
range: change.range,
text: change.text.slice(0, 50) + (change.text.length > 50 ? '...' : '') // Truncate for logs
}));
logEvent('TextChange', {
document: event.document.fileName,
changes,
timestamp: new Date().toISOString()
});
});
// Log completion acceptance (proxy via command if direct event unavailable)
const completionDisposable = vscode.commands.registerCommand('editor.action.inlineSuggest.commit', () => {
logEvent('AISuggestionAccepted', {
editor: vscode.window.activeTextEditor?.document.fileName,
timestamp: new Date().toISOString()
});
}); // Note: For Cursor's AI, this intercepts Tab accepts; test/adjust.
// Command to view logs
const viewLogsDisposable = vscode.commands.registerCommand('cursor-observability.viewLogs', () => {
vscode.workspace.openTextDocument(logFile).then(doc => {
vscode.window.showTextDocument(doc);
});
});
context.subscriptions.push(changeDisposable, completionDisposable, viewLogsDisposable);
logEvent('ExtensionActivated', { version: context.extension.packageJSON.version });
}
function logEvent(eventName: string, details: any) {
const logEntry = `[${new Date().toISOString()}] ${eventName}: ${JSON.stringify(details)}\n`;
outputChannel.append(logEntry);
fs.appendFileSync(logFile, logEntry);
}
export function deactivate() {
outputChannel.dispose();
}{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"outDir": "out",
"lib": ["es2020"],
"sourceMap": true,
"rootDir": "src",
"strict": true
},
"exclude": ["node_modules", ".vscode-test"]
}- Clone the repo, run
npm install,npm run compile. - Package:
vsce package(installvsceglobally). - Install in Cursor: Drag
.vsixto Extensions view or usecode --install-extension cursor-observability-0.1.0.vsix. - Activate: Reload Cursor. Use AI features (e.g., Tab autocomplete); logs appear in Output panel ("Cursor Observability") or
observability.log.- Example log:
[2026-02-11T12:00:00Z] AISuggestionAccepted: {"editor":"/path/to/file.ts","timestamp":"2026-02-11T12:00:00Z"}
- Example log:
- View logs: Run command "View Observability Logs" (Ctrl+Shift+P).
For advanced: Integrate OTel in TS (use @opentelemetry/api) to export traces, or hook deeper into Cursor's AI via custom modes.