Skip to content

Instantly share code, notes, and snippets.

@decagondev
Last active February 11, 2026 18:05
Show Gist options
  • Select an option

  • Save decagondev/537596cad861e5621586396802a60d62 to your computer and use it in GitHub Desktop.

Select an option

Save decagondev/537596cad861e5621586396802a60d62 to your computer and use it in GitHub Desktop.

Observability

Claude Code and Cursor

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.json for 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).

1. Python Observability Hook for Claude Code

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()

Usage in Claude Code:

  1. Save as .claude/hooks/observability_hook.py (make executable: chmod +x).
  2. Configure in .claude/settings.json (or via /hooks menu):
    {
      "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"}]}]
      }
    }
  3. 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 ConsoleSpanExporter with OTLPSpanExporter (e.g., to Langfuse: set endpoint and headers).

This gives under-the-hood visibility into agent lifecycles, bottlenecks, and errors.

2. TypeScript Observability Extension for Cursor (VS Code-Based)

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 onDidAcceptCompletionItem if 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.

package.json

{
  "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"
  }
}

src/extension.ts

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();
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2020",
    "outDir": "out",
    "lib": ["es2020"],
    "sourceMap": true,
    "rootDir": "src",
    "strict": true
  },
  "exclude": ["node_modules", ".vscode-test"]
}

Usage in Cursor:

  1. Clone the repo, run npm install, npm run compile.
  2. Package: vsce package (install vsce globally).
  3. Install in Cursor: Drag .vsix to Extensions view or use code --install-extension cursor-observability-0.1.0.vsix.
  4. 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"}
  5. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment