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:
- A project ignores secrets via
.gitignore (e.g. prod-dump.sql, customer-data.json, config/local.yaml).
- Someone later adds a
.graphifyignore to exclude media (*.png, *.pdf, …) and forgets to copy the .gitignore entries into it.
- graphify now ignores
.gitignore, indexes the secret file into the graph.
- 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)
- Merge both files (gitignore outer, graphifyignore inner/last-match-wins). Single change, eliminates the footgun.
- 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.
- 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.
Summary
When a directory contains a
.graphifyignore, graphify stops reading that directory's.gitignoreentirely — the two are not merged. A pattern present only in.gitignoreis silently dropped, so files the user believed were ignored get pulled into the knowledge graph.The README describes this as "If both exist,
.graphifyignoretakes 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.htmlembed file contents and are routinely committed. A realistic, easy-to-hit sequence:.gitignore(e.g.prod-dump.sql,customer-data.json,config/local.yaml)..graphifyignoreto exclude media (*.png,*.pdf, …) and forgets to copy the.gitignoreentries into it..gitignore, indexes the secret file into the graph.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-bearingsecret/token/password/credentialin the name) mitigates the obvious cases, but does not cover neutrally-named secrets likeprod-dump.sql,customer-data.json, orseed.yaml— precisely the files people rely on.gitignoreto exclude.Reproduction (verified on graphifyy 0.8.40)
build-artifact.mdreappears purely because a.graphifyignoreexists in the same directory — even though that.graphifyignoresays nothing about it.Root cause
graphify/detect.py,_load_graphifyignore():Per directory, it reads one or the other, never both.
Expected behavior
.gitignoreand.graphifyignoreshould be merged, not mutually exclusive — read.gitignorefirst, then append.graphifyignoreso 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.graphifyignorecan only ever exclude more, never silently re-include something.gitignoreexcluded.Suggested fixes (in order of preference)
~/.config/graphify/ignore, or honor git'score.excludesFile) so users don't have to duplicate media/secret patterns into every repo. There's currently no global-ignore mechanism at all..graphifyignoreshadows a non-empty sibling.gitignore, and fix the README wording — "takes priority" should read "replaces.gitignorein that directory (patterns are not merged)."Happy to send a PR for option 1 if that direction is acceptable.