grithdocs

Trust boundaries

What grith trusts, what it treats as hostile, and which crossing each control sits on.

grith has one enforcement boundary: the syscall boundary of the supervised process tree. This page is about what sits on each side of it, and about the smaller boundaries inside grith itself.

Three levels

LevelWhat is in itWhy
TrustedThe kernel, the grith binary, the daemon, the 18 filters, the built-in profilesIf any of these is compromised the game is over. grith cannot defend itself against itself.
UntrustedThe supervised tool, its libraries, the model driving it, its MCP servers and pluginsIt may be malicious, or merely mistaken. Either way its own judgement about safety is not load-bearing.
HostileEverything the tool reads - files, search results, provider responses, bytes off the networkPrompt injection lives here. This is what taint tracking exists for.

The 11 built-in profiles are compiled into the binary. There is no profile file for an agent to rewrite, and no user-authored profile to review.

The crossing that is enforced

Every syscall the supervised tree makes crosses from untrusted to trusted, and that is where the filters run. The tree is the tool plus its fork, clone and exec descendants; PTRACE_O_EXITKILL is always set, so a supervised process cannot outlive its supervisor by killing it.

The read that brings hostile data into the tool is not itself a barrier - it is scored like any other file operation, and what it changes is the taint state that later network and spawn calls are judged against.

Some actions never make the privileged syscall themselves. A supervised tool can ask a peer that already holds the authority - the docker daemon, an attached tmux pane, systemd-run on the session bus - and the real work then runs outside the tree, where nothing is intercepted. grith mitigates the known vectors rather than claiming closure: those spawns and control-socket connects escalate for review, and the D-Bus case is decided per method call rather than per connection. See Limitations for the residuals.

D-Bus: the decision moved off the connect

At connect(2) a socket path is all there is to judge, and "read the keyring" and "ask systemd to run this command outside supervision" are byte-identical syscalls. So a connect to a bus endpoint arms message inspection instead of escalating, and each outgoing method call is checked against an allowlist of (destination, interface, member) triples.

It is an allowlist, and it fails toward the older behaviour: an undecodable stream, an unreadable payload, or a method that is not listed all escalate the connection - exactly what connect-time enforcement did on its own. A decoder bug costs you a prompt, never a silent allow.

Inside grith: the daemon owns everything

grith exec is a thin client. The daemon is the single owner of the audit database (as its exclusive flock writer), the digest queue, the proxy and its filters, the session registry, the canary registry and the reputation table. The supervisor talks to it over loopback HTTP with a bearer token read from ~/.config/grith/daemon.token, which is mode 0600 and rotated on every daemon start.

There is no in-process fallback. If the daemon is unreachable, the wrong version, or refuses the token, grith exec fails and the target is never spawned. Capacity is reserved before the target starts, so a session-limit rejection can never arrive after code has already run.

The dashboard

The daemon serves the local dashboard on 127.0.0.1:3141 over plain HTTP - TLS is off by default, and loopback is the reason that is acceptable. Point [server] host at a routable address and the bind still succeeds, with a plaintext warning in the log, but the loopback guard answers every non-loopback peer 403 LOCALHOST_ONLY regardless. Three tiers of access:

  • Open reads - health, tier, licence status, proxy status. No token.
  • Sensitive reads - digest, audit, config, analytics, canaries, inventory. The dashboard token, sent in an x-grith-csrf header and compared in constant time.
  • Browser writes - the same token, checked again by a method-aware CSRF guard, with CORS locked to the dashboard's own origin, so another page cannot make the browser send it.

The browser gets that token through a single-use pair code carried in the URL fragment. A fragment is never sent with the page request, so the code stays out of server logs and referrers, and the page trades it for the token exactly once - which makes a later screenshot of the URL inert. The /ws/live WebSocket checks Origin against Host and compares its token in constant time.

What leaves the machine

No user data, by default or at community tier. A default install makes two unauthenticated GETs - the release check against GitHub, and the supervisor-profile overlay manifest from api.grith.ai - and neither carries anything beyond the version in a User-Agent. GRITH_NO_UPDATE_CHECK and [general] profile_update_check = false switch them off.

No audit records are uploaded, on any plan - that sync was retired. grith pro sync pulls team policies, shared configs and provider keys, and pushes the reputation table.

Four things have to be true before any of your data leaves: a paid plan, signed in, [general] audit_sync on, and recorded consent - which defaults on for an entitled, signed-in account, so grith analytics disable is the switch if you do not want it. What then goes is the analytics projection: counts and rollups with the operands stripped out, uploaded roughly every 30 seconds, plus one archive per sealed UTC day - written to private storage, encrypted under a KMS key grith manages, and kept 90 days.

Uploaded. Account, team, device and actor ids; session UUID; project label; profile and config hashes; supervised tool name; verdict, score and the filter ids that contributed; provider, model, token counts and cost; destination kind with a team-scoped HMAC; security event type and resolution.

Never uploaded, as a normative exclusion list in the frozen contract: command, arguments, arguments_summary, raw_path, raw_url, prompt, model_response, file_contents, source_code, payload_body, environment_values, task_context, and free-form decision reasons.

Two caveats worth having before you turn it on: project names travel in clear text and may themselves be commercially sensitive, and destinations are HMACed by default - clear destination labels are an owner or admin opt-in. Analytics has the full picture.

Files, and who can write them

PathWhatProtection
~/.config/grith/daemon.tokenIPC bearer for supervisor sessions0600, rotated on every daemon start
~/.config/grith/dashboard.tokenBrowser token0600, stable across restarts; delete it to rotate
~/.config/grith/license.keySigned licenceEd25519, public key compiled into the binary, verified locally with no network
~/.config/grith/provider-keys/Synced provider keysAES-256-GCM at rest, key derived from your grith API key
~/.local/share/grith/audit/audit.dbAudit logSHA-256 hash chain per row; a single writer holds an flock

Provider keys are sealed to the API key rather than to a passphrase or an OS keyring. Anyone holding the account's API key can decrypt them, and rotating the API key makes existing envelopes undecryptable. It is at-rest protection, not end-to-end encryption.

The audit chain is what makes tampering loud rather than impossible. Records hash into three frozen canonical forms, dispatched per row, so a database written across versions still verifies end to end. A break quarantines the chain: writes are refused and the daemon admits no new sessions, while grith audit diagnose keeps working so you can see what happened.

⚠️Permissions are not the control

Every file above is owned by your user, and the supervised tool runs as your user. Filesystem permissions do not separate them - the filters do. A read of grith's own state directory is scored like any other file read.

terminal
$ grith proxy test '{"type":"FileRead","path":"/home/u/.config/grith/license.key"}'

Proxy Test Result
==================================================
Tool call:   FileRead(/home/u/.config/grith/license.key)
Score:       8.5
Decision:    DENY
Reason:      Access to private key file; read access to grith's own state directory
Eval time:   0.03ms
Thresholds:  allow < 3, deny > 8
Filters:     18 active

Filter Breakdown:
  . operation-risk         0.0
  + path-match             4.0  [error]  Access to private key file
  + sensitive-path-heuristic   4.5  [error]  read access to grith's own state directory
  . allowlist              0.0
  . argument               0.0
  . capability             0.0

Exit code:   2 (deny)

See also

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