How the File Watcher System Monitors Conversation Changes in Claude Code

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, the observer is instantiated and stored in a variable to allow lifecycle management:

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:

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:

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:

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 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:

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, a persistent watcher is established:

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 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, 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 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.
  • 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →