Skip to content

bug: \codegraphcontext watch triggers re-indexing on filesystem events from paths matched by .cgcignore, causing stale/duplicate graph nodes #1275

Closed

Description

@prince-pokharna

🐛 Bug Report

Environment

  • CGC Version: v0.4.12 (latest PyPI)
  • OS: Linux / macOS / Windows (reproducible on all platforms)
  • Backend: KuzuDB (default), also reproducible with FalkorDB

Description

CodeGraphContext documents .cgcignore as the mechanism to exclude files and
directories from indexing (using .gitignore syntax). The codegraphcontext index
command correctly respects .cgcignore and skips matched paths during initial
indexing.

However, codegraphcontext watch — which uses watchdog to monitor the filesystem
for live changes and update the graph in real time — does NOT filter watchdog
filesystem events against the .cgcignore ruleset
. This means:

  1. Any write to a file under an ignored path (e.g. node_modules/, /build/,
    *.log) fires a watchdog FileModifiedEvent or FileCreatedEvent.
  2. The event handler triggers a re-index of that file.
  3. The .cgcignore check (if any) happens after the graph write attempt, not
    before the watchdog listener is registered.
  4. Result: ignored paths pollute the graph database with nodes for
    node_modules/ transitive dependencies, build artifacts, log files, etc.

Steps to Reproduce

# 1. Create a project with a .cgcignore
mkdir test_project && cd test_project
mkdir -p src node_modules/some-dep
echo "node_modules/" > .cgcignore
echo "def hello(): pass" > src/main.py
echo "def dep_func(): pass" > node_modules/some-dep/lib.py

# 2. Index and start watching
codegraphcontext index .
codegraphcontext watch .

# 3. In another terminal, write to an ignored path
echo "def new_dep(): pass" >> node_modules/some-dep/lib.py

# 4. Query the graph — the ignored file is now indexed
codegraphcontext analyze callers new_dep
# Expected: "No results (file is ignored)"
# Actual:   "new_dep" is found as a graph node from node_modules/

Root Cause

In src/codegraphcontext/, the file watcher (backed by watchdog) registers
an Observer on the target directory root without pre-filtering the observed
paths against the parsed .cgcignore ruleset (pathspec is a listed dependency
and is used during index, but its match_file() is not called in the event
handler that fires on FileModifiedEvent / FileCreatedEvent during watch).

This is analogous to Git itself — Git uses .gitignore to filter both
git add (index) and the working tree status checks. CGC currently only
filters the equivalent of git add but not the live event stream.

Expected Behavior

codegraphcontext watch should apply the same .cgcignore filtering logic to
every watchdog filesystem event before triggering any graph update. If a
changed file's path matches a .cgcignore pattern, the event should be silently
dropped — no re-indexing, no graph mutation, no log output for that file.

Proposed Fix

In the watchdog event handler class (likely in
src/codegraphcontext/watcher.py or similar):

import pathspec

class CGCEventHandler(FileSystemEventHandler):
    def __init__(self, indexer, ignore_spec: pathspec.PathSpec, root: str):
        self.indexer = indexer
        self.ignore_spec = ignore_spec  # Parsed .cgcignore
        self.root = root

    def _is_ignored(self, event_path: str) -> bool:
        """Return True if this path matches .cgcignore rules."""
        rel_path = os.path.relpath(event_path, self.root)
        return self.ignore_spec.match_file(rel_path)

    def on_modified(self, event):
        if event.is_directory or self._is_ignored(event.src_path):
            return
        self.indexer.reindex_file(event.src_path)

    def on_created(self, event):
        if event.is_directory or self._is_ignored(event.src_path):
            return
        self.indexer.index_file(event.src_path)

    def on_deleted(self, event):
        if event.is_directory or self._is_ignored(event.src_path):
            return
        self.indexer.remove_file_nodes(event.src_path)

The ignore_spec should be loaded once at watcher startup:

cgcignore_path = os.path.join(target_dir, ".cgcignore")
if os.path.exists(cgcignore_path):
    with open(cgcignore_path) as f:
        ignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
else:
    ignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", [])

Additional Improvements in Scope

  1. .cgcignore reload on change: If .cgcignore itself is modified while
    watch is running, the watcher should reload the pathspec — currently it
    would be stale for the rest of the session.
  2. Configurable additional ignore patterns via CLI flag:
    codegraphcontext watch . --ignore "*.test.py" --ignore "fixtures/" for
    one-off exclusions without editing .cgcignore.
  3. Add a regression test in tests/ that starts a watcher on a temp dir
    with a .cgcignore, writes to an ignored file, and asserts no new graph
    nodes were created.

Impact

This bug affects every user who relies on codegraphcontext watch for live
graph updates in projects with node_modules/, build/, dist/, or any
other large ignored directory. In a typical JavaScript/TypeScript project,
node_modules/ can contain 50,000+ files — a single npm install while
watching would queue 50,000 graph-write operations, degrading performance
severely and filling the graph with irrelevant dependency internals. This
also directly explains the performance degradation reported in issue #1076
("gets slower after 40%") for users who watch directories with large ignored
subtrees. The fix is contained to the watcher event handler and is
non-breaking for all existing usage.

I'd like to work on this issue — I'll fix the event handler filtering, add
the .cgcignore reload-on-change behavior, and write a regression test.
Could you assign this to me?

Activity

  1. github-actions commented on Jun 17, 2026

    @github-actions
    Contributor

    Hi! 👋 Join our CodeGraphContext Discord channel to collaborate: https://discord.gg/dR4QY32uYQ

  2. prince-pokharna commented on Jun 17, 2026

    @prince-pokharna
    Author

    Hey @maintainer,
    I have deeply gone through the project repo by local running n testing , and found this major bug after understanding and analysis of the code files and directory structure .

    I would like to contribute to the issue based on my skills and requirement of the issue.
    Please assign it to me.

  3. Shashankss1205 commented on Aug 1, 2026

    @Shashankss1205
    Collaborator

    Partial update from a source audit of core/watcher.py on main (c0e0bed).

    The event path described here does filter now — _is_supported_code_file calls _should_ignore before any graph write, so a change under an ignored path is skipped. That part appears to have been addressed since v0.4.12.

    What was still broken is the initial scan and disk sync. _iter_supported_files called discover_files_to_index without forwarding the cgcignore_path the handler was constructed with:

    files, _ = discover_files_to_index(
        self.repo_path,
        supported_extensions=set(supported),
    )

    So with watch_directory(path, cgcignore_path="/custom/ignore") discovery fell back to the repo-local .cgcignore only. The initial scan indexed files the custom ignore excluded, while subsequent change events for those same files were correctly skipped — leaving exactly the stale nodes this issue describes, reached by a different route: the graph keeps entries the user asked to exclude, and they never update on edit.

    Fixed in #1508 by forwarding cgcignore_path and re-applying the handler's own spec to the discovered set.

    Worth confirming against your original reproduction before closing — if you were seeing pollution without a custom ignore file, that is a different path than the one I fixed and this issue should stay open.

  4. Shashankss1205 commented on Aug 1, 2026

    @Shashankss1205
    Collaborator

    Following up on my earlier comment — I've now run the full reproduction and both halves are covered, so closing this.

    event-path filtering (_should_ignore):
       build/x.py     ignored=True
       secret.py      ignored=True
       ok.py          ignored=False
    initial-scan discovery (_iter_supported_files): ['ok.py']
    • Watchdog events are filtered before any graph write — core/watcher.py:115 gates on_created/on_modified (:349-355), with the guard at :253-254. This was fixed sometime after the v0.4.12 you reported against.
    • Initial scan / disk sync now honours the explicit .cgcignore too — that was still broken until fix: correct 13 bugs across query validation, MCP gateway, indexing and CLI (v0.5.4) #1508 (HEAD 6ad5edd), and was the remaining route by which ignored files reached the graph.

    So ignored paths no longer enter the graph via either path, and they no longer go stale on edit.

    Please reopen if you still see pollution on 0.5.4 — particularly if you're driving the watcher through MCP rather than the CLI, since tools/handlers/watcher_handlers.py:77 still hard-codes sync_on_start=True (noted on #1268), which exercises a slightly different startup path. Thanks for the thorough original report.

  5. moved this from Backlog tasks to Done in CGC Progress Boardon Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions