# How the File Watcher System Monitors Conversation Changes in Claude Code

> Discover how Claude Code's file watcher system monitors .claude directory changes. Learn about its dual-layer architecture using MutationObserver and chokidar for instant conversation updates.

- Repository: [Daniel Avila/claude-code-templates](https://github.com/davila7/claude-code-templates)
- Tags: internals
- Published: 2026-04-26

---

**The file watcher system uses a dual-layer architecture that combines a browser-side `MutationObserver` for real-time DOM changes with a Node.js `chokidar` filesystem watcher for the `.claude` directory, both feeding into a centralized hook pipeline that reacts instantly to conversation updates.**

The `davila7/claude-code-templates` repository implements a sophisticated file watcher system to monitor conversation changes without polling. Instead of repeatedly querying the server for new messages, the architecture leverages browser APIs and Node.js filesystem watchers to detect mutations immediately. This approach ensures that both UI-level conversation updates and disk-level file modifications trigger the appropriate hooks in real-time.

## Browser-Side Detection with MutationObserver

The Claude Code dashboard does not poll the server for new messages. Instead, it installs a lightweight `MutationObserver` on the DOM element containing the conversation, capturing every insertion as Claude streams replies.

### Creating the Observer in SearchModal.tsx

Inside [`dashboard/src/components/SearchModal.tsx`](https://github.com/davila7/claude-code-templates/blob/main/dashboard/src/components/SearchModal.tsx), the observer is instantiated and stored in a variable to allow lifecycle management:

```typescript
let observer: MutationObserver | null = null;
observer = new MutationObserver(() => {
  // a new node was inserted → conversation changed
  onConversationChanged();
});

```

Keeping the observer in a mutable reference allows the component to start and stop monitoring as the modal opens or closes, preventing unnecessary computation when the view is inactive.

### Targeting the Conversation Container

The observer attaches to a specific root element that contains all message bubbles:

```typescript
const conversationRoot = document.querySelector('#conversation-container');

```

Targeting `#conversation-container` is efficient because this is the only region of the page that mutates when Claude streams a reply. Watching this specific node eliminates the performance cost of observing the entire document.

### Configuring Mutation Observation

When the target is located, the observer begins watching with precise configuration options:

```typescript
if (conversationRoot) {
  observer.observe(conversationRoot, { childList: true, subtree: true });
}

```

Setting `childList: true` ensures the callback fires when direct children (new message bubbles) are added. The `subtree: true` option guarantees detection of nested changes, such as token-by-token streaming updates inside existing message containers.

### Handling DOM Mutations and Dispatching Events

When a mutation occurs, the `onConversationChanged` callback updates the local state and broadcasts a custom event for the hook system:

```typescript
function onConversationChanged() {
  // 1️⃣ update the local store
  setMessages(readMessagesFromDOM());
  // 2️⃣ optionally push a custom event for other parts of the app
  window.dispatchEvent(new CustomEvent('claude-conversation-updated'));
}

```

This custom event serves as the entry point for the **hook system**, allowing scripts like [`context-timeline.py`](https://github.com/davila7/claude-code-templates/blob/main/context-timeline.py) to record conversation turns or update telemetry data in `~/.claude/performance.csv`.

### Cleanup and Memory Management

To prevent memory leaks when the component unmounts, the observer is explicitly disconnected:

```typescript
return () => {
  if (observer) observer.disconnect();
};

```

## File-System Watcher for the .claude Directory

While the `MutationObserver` handles UI-level changes, the CLI side monitors the physical `.claude` directory for file additions, modifications, or deletions using `fs.watch` via the `chokidar` library.

### Implementing the Node.js Watcher

In [`cli-tool/bin/create-claude-config.js`](https://github.com/davila7/claude-code-templates/blob/main/cli-tool/bin/create-claude-config.js), a persistent watcher is established:

```javascript
const chokidar = require('chokidar');
const watcher = chokidar.watch(path.join(process.cwd(), '.claude'), {
  ignored: /(^|[\/\\])\../, // ignore dotfiles except the .claude folder itself
  persistent: true
});
watcher.on('add', file => triggerHook('FileAdded', file));
watcher.on('change', file => triggerHook('FileChanged', file));
watcher.on('unlink', file => triggerHook('FileRemoved', file));

```

The `ignored` regex excludes dotfiles to prevent infinite loops when hooks write metadata, while `persistent: true` keeps the Node.js process alive to continue monitoring.

## Integrating Both Watchers into the Hook Pipeline

Both observation mechanisms feed into the same **central hook engine**, ensuring comprehensive coverage of all conversation-related changes.

**UI Changes Flow:**
- Claude streams a reply → DOM node inserted → `MutationObserver` fires → `claude-conversation-updated` event dispatched → Hook scripts in `.claude/hooks/*.js` or `.claude/hooks/*.py` execute (e.g., [`context-timeline.py`](https://github.com/davila7/claude-code-templates/blob/main/context-timeline.py) appends to the timeline JSON).

**Disk Changes Flow:**
- User edits a hook script or settings file → `chokidar` detects the change → `triggerHook` called with `FileChanged` event → Hook system reloads the updated script or regenerates config.

This dual-layer approach guarantees that **any** change affecting the conversation—whether a new message bubble in the browser or a modified Python hook on disk—triggers the appropriate pipeline instantly.

## Summary

- **Dual Architecture:** The system combines a browser `MutationObserver` with a Node.js `chokidar` filesystem watcher to monitor both UI and disk changes.
- **DOM Observation:** In [`dashboard/src/components/SearchModal.tsx`](https://github.com/davila7/claude-code-templates/blob/main/dashboard/src/components/SearchModal.tsx), a `MutationObserver` watches `#conversation-container` with `childList: true` and `subtree: true` to catch message insertions and streaming updates.
- **Filesystem Monitoring:** The CLI uses `chokidar` in [`cli-tool/bin/create-claude-config.js`](https://github.com/davila7/claude-code-templates/blob/main/cli-tool/bin/create-claude-config.js) to watch the `.claude` directory, firing on `add`, `change`, and `unlink` events while ignoring dotfiles.
- **Hook Integration:** Both watchers dispatch events that the hook system consumes, enabling real-time reactions from scripts in `.claude/hooks/` such as [`context-timeline.py`](https://github.com/davila7/claude-code-templates/blob/main/context-timeline.py).
- **Memory Safety:** The `MutationObserver` is properly disconnected on component unmount to prevent memory leaks.

## Frequently Asked Questions

### What is the difference between the MutationObserver and the chokidar watcher?

The **MutationObserver** runs in the browser and detects changes to the conversation DOM, such as new message bubbles or streaming text updates. The **chokidar** watcher runs in the Node.js CLI process and monitors the physical `.claude` directory for file operations like hook script modifications or new configuration files. The observer handles UI-level mutations, while the filesystem watcher handles disk-level changes.

### Where is the MutationObserver instantiated in the codebase?

The `MutationObserver` is created in [`dashboard/src/components/SearchModal.tsx`](https://github.com/davila7/claude-code-templates/blob/main/dashboard/src/components/SearchModal.tsx). It is stored in a variable scoped to the component, attached to the `#conversation-container` DOM element, and disconnected during cleanup to prevent memory leaks when the modal closes.

### How do hook scripts get notified of conversation updates?

Hook scripts receive notifications through two pathways. For conversation changes, the browser dispatches a `CustomEvent` named `claude-conversation-updated` that listeners in `.claude/hooks/*.js` or `.claude/hooks/*.py` can intercept. For file changes, the Node.js watcher calls `triggerHook` with events like `FileAdded` or `FileChanged` when the `chokidar` watcher detects modifications in the `.claude` directory.

### Why does the file watcher ignore dotfiles in the .claude directory?

The `chokidar` configuration uses the regex `/(^|[\/\\])\../` to ignore dotfiles, preventing the watcher from triggering hooks when hidden files change. This avoids infinite loops where hooks themselves write metadata or temporary files, ensuring only relevant user-facing files (hooks, configs, and generated scripts) trigger reactions.