The three-phase pipeline
Eighteen filters in three phases, parallel within a phase, with a deny in an early phase ending the evaluation there.
Every intercepted call runs through 18 filters, arranged in three phases. Filters inside a phase run concurrently; the phases run in order. All 18 are registered and running on a default install - every config toggle in this area is an off switch, not an on switch.
The order below is the registration order, and it is exactly the order
grith proxy test prints.
| Phase | Filters |
|---|---|
| 1 - static | operation-risk, path-match, sensitive-path-heuristic, allowlist, argument, capability |
| 2 - pattern | secret-scan, command, destructive-action, egress-policy, dlp-gate, canary |
| 3 - context | reputation, behavioural, taint, session-containment, rate-limit, egress-rate |
Phase 1 looks only at the shape of the call - operation type, path string, argument structure. No content scanning, no history.
Phase 2 does the string work: 1618 secret patterns, a shell parser, destructive-command recognition, destination policy, outbound payload inspection, canary matching.
Phase 3 reaches into session state - what this session has already read, how fast it is going, whether it is under containment, how this destination is regarded.
An early deny stops everything after it
At the end of each phase grith sums what has matched so far. If that sum is already past the deny threshold, it returns immediately. Later phases never run, and the meta-rules never run either.
This is visible in real output. A read of an SSH private key is settled by two phase-1
filters, so only the six static filters print under an 18 active header:
$ grith proxy test '{"type":"FileRead","path":"/home/u/.ssh/id_rsa"}'
Proxy Test Result
==================================================
Tool call: FileRead(/home/u/.ssh/id_rsa)
Score: 9.0
Decision: DENY
Reason: Access to SSH private key; read access to key/certificate file
Eval time: 0.03ms
Thresholds: allow < 3, deny > 8
Filters: 18 active
Filter Breakdown:
. operation-risk 0.0
+ path-match 5.0 [critical] Access to SSH private key
+ sensitive-path-heuristic 4.0 [error] read access to key/certificate file
. allowlist 0.0
. argument 0.0
. capability 0.0
Exit code: 2 (deny)rm -rf / gets one phase further: operation-risk scores it 1.0, which does not clear
the line on its own, so phase 2 runs and destructive-action adds 9.0. The composite is
10.0 and it is settled at the end of phase 2, still without phase 3 or the meta-rules.
A call that survives to the end prints all 18 lines:
$ grith proxy test '{"type":"FileRead","path":"/etc/hosts"}'
Score: 0.5
Decision: ALLOW
Reason: Score 0.5 below allow threshold
Eval time: 1.50ms
Thresholds: allow < 3, deny > 8
Filters: 18 active
Filter Breakdown:
. operation-risk 0.0
. path-match 0.0
+ sensitive-path-heuristic 0.5 [notice] read reads a system config path
. allowlist 0.0
. argument 0.0
. capability 0.0
. secret-scan 0.0
. command 0.0
. destructive-action 0.0
. egress-policy 0.0
. dlp-gate 0.0
. canary 0.0
. reputation 0.0
. behavioural 0.0
. taint 0.0
. session-containment 0.0
. rate-limit 0.0
. egress-rate 0.0
Exit code: 0 (allow)After phase 3
The meta-rules run once, on the whole set of results, and adjust the total. There are
five of them and they exist to fix specific false positives and false negatives - a
node_modules listing during dependency resolution comes down 3.0; an HTTP request made
after reading a .env file goes up 5.0.
Then the total is routed. See composite scoring.
Why a filter never fails a call
A filter that errors is logged and treated as no match. It cannot fail the call open or closed - it simply contributes nothing, and the other 17 still run. Nothing in the pipeline depends on any single filter being correct.