# Async Hook Pattern in Claude Code Stop Hooks: How It Prevents Blocking

> Discover the async hook pattern in Claude Code's Stop hook. Learn how it prevents UI freezing by handling I/O asynchronously in background processes, ensuring responsiveness.

- Repository: [Letta/claude-subconscious](https://github.com/letta-ai/claude-subconscious)
- Tags: deep-dive
- Published: 2026-03-26

---

**The async hook pattern prevents Claude Code from freezing by reading input asynchronously, spawning a detached background worker process to handle heavy I/O operations, and exiting the hook immediately so the UI remains responsive.**

Claude Code executes user-generated Stop hooks after every code generation step, and the `letta-ai/claude-subconscious` repository implements an async hook pattern that ensures these hooks never block the main execution thread. By decoupling heavy networking tasks from the hook's lifecycle, the UI remains fluid even when performing long-running operations like sending messages to the Letta service.

## How the Async Hook Pattern Works

### Asynchronous Input Consumption

In [`scripts/send_messages_to_letta.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/send_messages_to_letta.ts), the hook begins by reading the JSON payload from `stdin` using the `readHookInput` async function (lines 81‑100). This asynchronous approach allows the Node.js event loop to continue processing other tasks while waiting for the input stream to complete, preventing the initial read from becoming a blocking operation.

### Detached Background Worker Architecture

Once the hook gathers the necessary data such as the transcript and conversation ID, it delegates heavy I/O operations to a separate process. The `spawnSilentWorker` utility in [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts) (lines 22‑78) creates a detached child process via `child.unref()`, ensuring the worker survives parent termination and runs independently of Claude Code's main thread. On Windows, this optionally leverages `silent‑launcher.exe` to hide console windows while maintaining the detached state.

### Immediate Process Termination

After launching the background worker, the hook exits immediately. As shown in [`send_messages_to_letta.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_messages_to_letta.ts) at line 333, the process logs completion and terminates, returning control to Claude Code instantly. The heavy lifting—network calls to Letta, SDK messaging, and state file updates—occurs within [`scripts/send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/send_worker_sdk.ts) (lines 11‑55), which executes asynchronously in its own process without impacting the editor's responsiveness.

## Implementation Example

### Minimal Async Stop Hook Skeleton

```typescript
#!/usr/bin/env npx tsx
import * as path from 'path';
import { spawnSilentWorker } from './conversation_utils.js';

async function readInput(): Promise<any> {
  return new Promise((resolve, reject) => {
    let data = '';
    process.stdin.on('data', chunk => data += chunk);
    process.stdin.on('end', () => resolve(JSON.parse(data)));
    process.stdin.on('error', reject);
  });
}

async function main() {
  const input = await readInput();
  const payloadFile = '/tmp/payload.json';
  // Write payload preparation logic here
  
  const worker = spawnSilentWorker(
    path.join(__dirname, 'my_worker.ts'),
    payloadFile,
    process.cwd()
  );
  console.log('Hook done, worker PID', worker.pid);
  process.exit(0);
}

main();

```

### Background Worker Implementation

The worker script handles the actual processing:

```typescript
#!/usr/bin/env npx tsx
import * as fs from 'fs';

const payloadPath = process.argv[2];
const payload = JSON.parse(fs.readFileSync(payloadPath, 'utf-8'));

async function doWork() {
  // Perform long-running operations: HTTP requests, SDK calls, etc.
}

doWork().finally(() => fs.unlinkSync(payloadPath));

```

This pattern mirrors the production implementation in [`send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_worker_sdk.ts), where the worker parses the payload, initializes the Letta SDK session, and updates state files without blocking the parent hook.

## Summary

- The **async hook pattern** reads input asynchronously via `readHookInput` in [`scripts/send_messages_to_letta.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/send_messages_to_letta.ts) to avoid blocking on I/O during initialization.
- **Detached processes** created by `spawnSilentWorker` in [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts) (lines 22‑78) execute heavy tasks independently using `child.unref()`.
- **Immediate exit** after worker spawning ensures Claude Code resumes its workflow instantly, with background workers handling network latency and state persistence.
- The architecture separates the **hook orchestration** in [`send_messages_to_letta.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_messages_to_letta.ts) from **task execution** in [`send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_worker_sdk.ts), maintaining UI fluidity regardless of backend operation duration.

## Frequently Asked Questions

### What is the async hook pattern in Claude Code Stop hooks?

The async hook pattern is an architectural approach where Stop hooks read input asynchronously, spawn detached background workers to handle resource-intensive operations, and terminate immediately to return control to Claude Code. As implemented in the `letta-ai/claude-subconscious` repository, this pattern ensures that network calls and SDK operations never freeze the editor interface.

### How does spawnSilentWorker prevent blocking?

The `spawnSilentWorker` function in [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts) (lines 22‑78) creates a detached child process using Node.js `spawn` with `detached: true` and calls `child.unref()`. This decouples the worker's lifecycle from the parent hook, allowing Claude Code to continue execution while the worker performs time-consuming tasks like HTTP requests to the Letta service.

### Why use a detached process instead of async/await in the main hook?

While async/await prevents blocking the JavaScript event loop, it does not protect against process-level latency. Claude Code waits for the entire hook process to exit before continuing. By spawning a detached worker and exiting immediately, the hook signals completion to Claude Code within milliseconds, while the heavy lifting continues in a separate process that can tolerate longer execution times without impacting the UI.

### Where does the heavy processing occur in the letta-ai/claude-subconscious implementation?

All network-bound operations occur in [`scripts/send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/send_worker_sdk.ts) (lines 11‑55), which runs as a detached background worker. This script handles payload parsing, Letta SDK session initialization, message transmission, and state file updates. The main hook in [`send_messages_to_letta.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_messages_to_letta.ts) only prepares the payload and launches the worker, ensuring zero blocking time in the critical path.