# How Watch Exclude Patterns Function in AgentsView: Filtering File-System Events with fsnotify

> Learn how AgentsView filters file system events using watch exclude patterns. Discover how to effectively use glob-style strings to ignore specific directories and files for efficient monitoring.

- Repository: [Kenn Software/agentsview](https://github.com/kenn-io/agentsview)
- Tags: how-to-guide
- Published: 2026-06-20

---

**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`](https://github.com/kenn-io/agentsview/blob/main/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`](https://github.com/kenn-io/agentsview/blob/main/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.Clean` to resolve relative components like `.` and `..`
- Removes duplicates and empty strings, including literal `"."` entries

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

1. Computes the relative path from the root
2. Splits the path into components
3. 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.

```go
// 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`:

```go
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`](https://github.com/kenn-io/agentsview/blob/main/internal/sync/watcher_test.go) verify that files inside excluded directories never trigger callbacks. The following test pattern demonstrates this isolation:

```go
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`](https://github.com/kenn-io/agentsview/blob/main/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**: `normalizeExcludePatterns` in [`internal/sync/watcher.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sync/watcher.go) sanitizes exclude strings during `NewWatcher` construction 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 `WatchRecursiveBudgeted` walk and also blocked at runtime via `watchIfDir` when new directories appear.
- **Performance optimization**: Returning `filepath.SkipDir` prevents 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`](https://github.com/kenn-io/agentsview/blob/main/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`](https://github.com/kenn-io/agentsview/blob/main/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.