Skip to content

.graphifyignore silently replaces .gitignore instead of merging — secrets ignored only in .gitignore get indexed into the graph #1363

Description

@suenot

Summary

When a directory contains a .graphifyignore, graphify stops reading that directory's .gitignore entirely — the two are not merged. A pattern present only in .gitignore is silently dropped, so files the user believed were ignored get pulled into the knowledge graph.

The README describes this as "If both exist, .graphifyignore takes priority", which reads like merge-with-precedence. The actual behavior is full replacement of that directory's .gitignore. Those are very different, and the difference is a security footgun.

Why this is a security problem

graph.json / GRAPH_REPORT.md / graph.html embed file contents and are routinely committed. A realistic, easy-to-hit sequence:

  1. A project ignores secrets via .gitignore (e.g. prod-dump.sql, customer-data.json, config/local.yaml).
  2. Someone later adds a .graphifyignore to exclude media (*.png, *.pdf, …) and forgets to copy the .gitignore entries into it.
  3. graphify now ignores .gitignore, indexes the secret file into the graph.
  4. The graph artifacts get committed/pushed — secret leaks to a (possibly public) repo.

Two sources of truth for "what to ignore," where adding the second one silently disables the first, is exactly the shape of mistake that leaks data.

The built-in sensitive-file detection (_is_sensitive: .env, *.pem, *.key, id_rsa, files with load-bearing secret/token/password/credential in the name) mitigates the obvious cases, but does not cover neutrally-named secrets like prod-dump.sql, customer-data.json, or seed.yaml — precisely the files people rely on .gitignore to exclude.

Reproduction (verified on graphifyy 0.8.40)

T=$(mktemp -d)
printf 'build-artifact.md\n' > "$T/.gitignore"   # gitignore excludes this non-secret file
printf '# keep\n'  > "$T/keep.md"
printf '# art\n'   > "$T/build-artifact.md"
printf 'x\n'       > "$T/pic.png"

python -c "from graphify.detect import detect; from pathlib import Path; \
print(sorted(p.split('/')[-1] for p in detect(Path('$T'))['files'].get('document',[])))"
# -> ['keep.md']                      (build-artifact.md correctly excluded by .gitignore)

printf '*.png\n' > "$T/.graphifyignore"           # add a .graphifyignore that ONLY lists *.png
python -c "from graphify.detect import detect; from pathlib import Path; \
print(sorted(p.split('/')[-1] for p in detect(Path('$T'))['files'].get('document',[])))"
# -> ['build-artifact.md', 'keep.md']  (.gitignore is now IGNORED; build-artifact.md re-appears)

build-artifact.md reappears purely because a .graphifyignore exists in the same directory — even though that .graphifyignore says nothing about it.

Root cause

graphify/detect.py, _load_graphifyignore():

# Prefer .graphifyignore; fall back to .gitignore so projects that already
# maintain a .gitignore get sensible defaults without duplicating it (#945).
ignore_file = d / ".graphifyignore"
if not ignore_file.exists():
    ignore_file = d / ".gitignore"

Per directory, it reads one or the other, never both.

Expected behavior

.gitignore and .graphifyignore should be merged, not mutually exclusive — read .gitignore first, then append .graphifyignore so its patterns win on conflicts via the existing last-match-wins semantics. That matches the natural mental model ("graphify additionally ignores these") and removes the footgun: adding a .graphifyignore can only ever exclude more, never silently re-include something .gitignore excluded.

Suggested fixes (in order of preference)

  1. Merge both files (gitignore outer, graphifyignore inner/last-match-wins). Single change, eliminates the footgun.
  2. A global ignore (e.g. ~/.config/graphify/ignore, or honor git's core.excludesFile) so users don't have to duplicate media/secret patterns into every repo. There's currently no global-ignore mechanism at all.
  3. If replacement semantics must stay: at minimum, emit a loud warning when a .graphifyignore shadows a non-empty sibling .gitignore, and fix the README wording — "takes priority" should read "replaces .gitignore in that directory (patterns are not merged)."

Happy to send a PR for option 1 if that direction is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions