In the previous chapter, Enterprise Policy (MDM) Integration, we learned how to lock down settings using corporate policies. At this point, our application can read settings from everywhereβuser files, project files, and system registriesβwhen it starts up.
But what happens if you change a file after the application has started?
Imagine this scenario:
verbose logging..claude/settings.json and change "verbose": false to "true".That is a bad user experience. We want Live Change Detection.
We want the application to behave like a security guard watching a monitor. The moment a file is modified, added, or deleted, the application should instantly wake up, re-read the settings, and apply them.
We use a library called chokidar to monitor the file system. It listens for events like change, add, or unlink (delete).
This is the trickiest part.
Sometimes, the application itself modifies the settings file (for example, if you run a command like claude config --set theme=dark).
This is unnecessary work (and potentially an infinite loop). We need a way to distinguish between "You changed it" (External) and "I changed it" (Internal).
When you "save" a file in some editors, they don't just write to the file. They often delete the old file and create a new one instantly. If our watcher is too fast, it might panic and say "The settings are gone!", only for them to reappear 1 millisecond later. We need a "Grace Period" to handle this.
From the perspective of the rest of the application, the changeDetector is a simple subscription service.
You don't need to tell it which files to watch; it figures that out based on the logic we learned in Settings Cascade & Resolution.
Here is how a component listens for updates:
import { settingsChangeDetector } from './changeDetector'
// Subscribe to updates
const unsubscribe = settingsChangeDetector.subscribe((source) => {
console.log(`Something changed in: ${source}`)
// Now we should re-load the settings!
reloadSettings()
})
If you edit ~/.claude/settings.json, the console will log:
"Something changed in: userSettings"
Let's look at the flow of data when a file is modified.
The logic resides in src/settings/changeDetector.ts. Let's break it down.
We ask chokidar to watch the directories where our settings live. We use a stabilityThreshold to ensure we don't read a file while it is still being written to.
// changeDetector.ts (Simplified)
watcher = chokidar.watch(dirs, {
ignoreInitial: true, // Don't trigger for existing files at startup
awaitWriteFinish: {
stabilityThreshold: 1000, // Wait 1s for writes to finish
},
})
watcher.on('change', handleChange)
watcher.on('unlink', handleDelete)
This logic is split into internalWrites.ts. When the app writes a file, it "marks" it with a timestamp. When the watcher sees a change, it checks if there is a recent mark.
Step A: Marking the Write
// internalWrites.ts
const timestamps = new Map<string, number>()
export function markInternalWrite(path: string): void {
// "I am writing to this file right now"
timestamps.set(path, Date.now())
}
Step B: Checking the Mark
// changeDetector.ts
function handleChange(path: string): void {
// If we marked this file less than 5 seconds ago...
if (consumeInternalWrite(path, 5000)) {
// ...ignore this event. It's just an echo.
return
}
// Otherwise, it's a real user change!
notifyChange(getSourceForPath(path))
}
To solve the issue of editors deleting files temporarily during save, we don't react to deletions immediately. We set a timer.
// changeDetector.ts (Simplified)
function handleDelete(path: string): void {
// Wait a bit before panicking
const timer = setTimeout(() => {
// If we are still here, the file is truly gone.
notifyChange(getSourceForPath(path))
}, DELETION_GRACE_MS)
pendingDeletions.set(path, timer)
}
If the file is added back before the timer runs out, handleAdd cancels the timer!
// changeDetector.ts (Simplified)
function handleAdd(path: string): void {
const pendingTimer = pendingDeletions.get(path)
if (pendingTimer) {
// It's back! Cancel the deletion alert.
clearTimeout(pendingTimer)
pendingDeletions.delete(path)
// Treat it as a modification instead
handleChange(path)
}
}
As discussed in Chapter 4, Enterprise policies live in the Registry, not in files. chokidar cannot watch the Registry.
For this, we use a simple polling mechanism (checking every 30 minutes).
// changeDetector.ts (Simplified)
mdmPollTimer = setInterval(async () => {
// 1. Get current values
const current = await refreshMdmSettings()
// 2. Compare with last known values
if (JSON.stringify(current) !== lastMdmSnapshot) {
notifyChange('policySettings')
}
}, 30 * 60 * 1000) // 30 minutes
We have built a responsive system.
However, all this watching and reloading comes at a cost. Reading files from the disk is slow. If we have 10 different parts of the application asking for settings, we don't want to read the disk 10 times.
In the final chapter, we will learn how to make this efficient.
Generated by Code IQ