How Watch Exclude Patterns Function in AgentsView: Filtering File-System Events with fsnotify
AgentsView uses a slice of exclude patterns passed to NewWatcher that normalizes glob-style strings (e.g., .git, node_modules) and checks every file-system path against them via filepath.Match before adding directories to the watch list or reporting change events.
AgentsView is an open-source tool that monitors session directories for changes using the fsnotify library. To prevent noisy or irrelevant directories from triggering events, the watcher accepts configurable exclude patterns that are evaluated during both the initial recursive walk and runtime event processing. The implementation lives primarily in internal/sync/watcher.go and supports standard glob wildcards (*, ?, and character classes).
Core Implementation in internal/sync/watcher.go
The exclude mechanism centers on the Watcher struct defined in internal/sync/watcher.go. When you instantiate a watcher, the patterns are normalized once and stored for efficient lookup during the watch lifecycle.
Pattern Normalization During Construction
The NewWatcher function (lines 47-64) accepts an excludes slice and immediately passes it through normalizeExcludePatterns. This utility function (lines 222-236) performs three critical sanitization steps:
- Trims whitespace from each entry
- Applies
filepath.Cleanto resolve relative components like.and.. - Removes duplicates and empty strings, including literal
"."entries
excludes := []string{".git", "node_modules", " vendor "}
w, err := sync.NewWatcher(200*time.Millisecond, callback, excludes)
// Internally stores: []string{".git", "node_modules", "vendor"}
This normalization ensures that patterns match consistently regardless of how they are formatted in configuration files or CLI arguments.
The Exclude Check Logic
Before any path is added to the watch list, shouldExcludeForRoot (lines 82-115) evaluates whether it matches an exclude pattern. This function receives both the full file-system path and the root of the watch tree, then:
- Computes the relative path from the root
- Splits the path into components
- Matches each component (and the full relative path) against the stored exclude patterns using
filepath.Match
Because filepath.Match supports glob syntax, you can use patterns like vendor/** or *.tmp to filter specific file types or directory trees.
How Exclusion Works During Directory Traversal
The exclude logic operates at two distinct phases: during the initial recursive directory walk and when handling runtime file-system events.
Recursive Walk Handling
When WatchRecursiveBudgeted walks the session directory tree, it calls shouldExcludeForRoot for every directory encountered. If a directory matches an exclude pattern, the function returns filepath.SkipDir (lines 99-102), pruning that entire subtree from the watcher without consuming file descriptors or memory for its contents.
// From watcher.go lines 99-102
if shouldExclude(path) {
return filepath.SkipDir
}
This early termination is essential for performance when watching large codebases that contain dependency directories like node_modules or .git.
Runtime Event Filtering
After the initial setup, the watchIfDir helper (lines 93-99) intercepts file-system events that indicate new directories. If a newly created directory matches an exclude pattern, the function returns (true, true), signaling to the caller that the path is excluded and should not be added to the watch list. Consequently, any subsequent changes within that directory tree are ignored entirely.
Practical Usage and Configuration
You can configure exclude patterns programmatically via the NewWatcher constructor or through the CLI when starting a session watch.
Programmatic Configuration
Pass a slice of strings as the third argument to NewWatcher:
package main
import (
"fmt"
"time"
"github.com/kenn-io/agentsview/internal/sync"
)
func main() {
excludes := []string{".git", "node_modules", "*.log"}
w, err := sync.NewWatcher(200*time.Millisecond, func(paths []string) {
fmt.Println("changed:", paths)
}, excludes)
if err != nil {
panic(err)
}
// Walk the tree and start watching
_, _, err = w.WatchRecursive("/home/alice/agentsview_sessions")
if err != nil {
panic(err)
}
w.Start()
// ... w.Stop() on shutdown
}
Testing Exclude Behavior
The unit tests in internal/sync/watcher_test.go verify that files inside excluded directories never trigger callbacks. The following test pattern demonstrates this isolation:
func TestWatcherExcludes(t *testing.T) {
tmp := t.TempDir()
gitDir := filepath.Join(tmp, ".git")
require.NoError(t, os.Mkdir(gitDir, 0o755))
ignoredFile := filepath.Join(gitDir, "config")
require.NoError(t, os.WriteFile(ignoredFile, []byte("data"), 0o644))
changed := make(chan []string, 1)
w, err := sync.NewWatcher(10*time.Millisecond, func(p []string) {
changed <- p
}, []string{".git"})
require.NoError(t, err)
_, _, err = w.WatchRecursive(tmp)
require.NoError(t, err)
w.Start()
// Modify the ignored file
require.NoError(t, os.WriteFile(ignoredFile, []byte("new"), 0o644))
select {
case ev := <-changed:
t.Fatalf("unexpected change event: %v", ev)
case <-time.After(50 * time.Millisecond):
// Success - excluded path was ignored
}
w.Stop()
}
CLI Integration
The cmd/agentsview/session_watch.go file bridges user configuration to the watcher. It parses the --exclude flag (which can be specified multiple times) and passes the resulting slice to NewWatcher, allowing users to specify exclusions without modifying code.
Summary
- Normalization happens once:
normalizeExcludePatternsininternal/sync/watcher.gosanitizes exclude strings duringNewWatcherconstruction by cleaning paths and deduplicating entries. - Glob patterns supported: The matching logic uses
filepath.Match, enabling wildcards like*and?for flexible filtering. - Two-phase filtering: Excluded directories are skipped during the initial
WatchRecursiveBudgetedwalk and also blocked at runtime viawatchIfDirwhen new directories appear. - Performance optimization: Returning
filepath.SkipDirprevents the watcher from descending into excluded subtrees, conserving system resources.
Frequently Asked Questions
How do I specify multiple exclude patterns when starting AgentsView?
You can pass multiple patterns using the --exclude flag repeatedly. According to the CLI entry point in cmd/agentsview/session_watch.go, these values are collected into a slice and passed directly to NewWatcher. For example: agentsview watch --exclude=".git" --exclude="node_modules" --exclude="*.tmp" /path/to/session.
Can I use glob patterns like ** or character classes in exclude strings?
The matching engine relies on Go's filepath.Match function, which supports * (match any sequence), ? (match single character), and character ranges like [0-9]. However, ** (recursive glob) is not supported by filepath.Match; you should specify directory names like vendor or build which will match at any depth due to the component-wise comparison in shouldExcludeForRoot.
Why are changes still being detected in my excluded directory?
Ensure the pattern exactly matches the directory name as it appears on disk. The normalizeExcludePatterns function calls filepath.Clean, which strips trailing slashes, but patterns are case-sensitive. Also verify that you passed the excludes slice to NewWatcher before calling WatchRecursive, as patterns cannot be added dynamically after the watcher starts.
Where does the actual file-system notification happen if a file is not excluded?
When a path passes the exclude check, the underlying fsnotify watcher adds the path to its internal observation set. The Watcher struct in internal/sync/watcher.go wraps this low-level observer and batches events through a callback function provided during construction, firing only for non-excluded paths.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →