grithdocs

Supervisor-only assessment

grith enforces on one path. This is what that path carries, what has been closed, and what has not.

grith v0.3.1 has a single enforcement path. There is no second sandbox behind the supervisor: if ptrace and seccomp-BPF do not catch a call, nothing does. An internal assessment in early 2026 asked whether that one path could carry every control grith claims. This page reports where that stands on v0.3.1 - what the supervisor carries on its own, what has been closed since, and what is still open.

If you are auditing grith, read this alongside Limitations.

What the supervisor carries on its own

ControlOn v0.3.1
operation-risk, path-match, sensitive-path-heuristicFull. The path is resolved from the syscall itself.
argument, secret-scan, commandFull. They read the command line and argv the supervisor already reconstructs.
destructive-action, dlp-gate, canaryFull over the mapped call types.
egress-policy, reputation, egress-rateFull for connects, HTTP requests, and datagram sends that name a destination. Bytes on an already-connected socket are not re-evaluated per send.
behavioural, rate-limit, session-containment, taintFull. All four key on the supervisor's own call stream, scoped per session.
allowlistWorks, but starts empty. It carries only what your session grants and your saved allowlist put in it.
capabilityMechanism works. Ships with no grants, and with no grants the filter is permissive.
Meta-rules and composite scoringFull. They operate on filter output, so they are unaffected by how the call was captured.
Queue, human review, audit trailFull. Every supervisor evaluation goes through the same decision model and the same hash-chained record.

Closed since the assessment

Filter state is session-scoped. taint, rate-limit and behavioural key on a scope attached to every call - the conversation id where there is one, otherwise the session's own UUID. The taint registry was a global path map; it no longer is, and it expires at one hour. Cross-session contamination is gone.

Datagram sends with an explicit destination are scored. A sendto naming an arbitrary address used to be dropped as noise, which let a payload leave with no evaluation and no audit record without even needing a connect. It now surfaces as a connect and runs the egress path. Raw AF_PACKET sends do the same, and raw socket creation is denied at socket() for AF_PACKET and for netlink families beyond routing.

Abstract-namespace unix sockets are matched. They render as unix:@<name> at classification, so a control socket can no longer hide in the abstract namespace.

The delegating-binary classifier is no longer basename-only. A spawn is matched by argv basename, by canonical path after symlink resolution, and by content SHA-256 against a set pinned at session start - which defeats both ln -s systemd-run x and cp /usr/bin/systemd-run /tmp/x.

D-Bus is decided per method call. A connect to a bus endpoint arms message inspection instead of escalating the whole connection, and each method call is checked against an allowlist.

Escape enforcement is on by default. enforce_authority_delegating_spawn and enforce_control_socket_connect both default to true, and a call that would be enforced deliberately skips the session-allowlist short-circuit - otherwise a profile that lists docker or tmux as routine would make the whole mechanism a silent no-op.

Interceptor failure terminates the tree. If the interceptor errors, the supervisor terminates every tracee rather than exiting cleanly and leaving them running unsupervised. PTRACE_O_EXITKILL covers the other direction: a supervisor crash SIGKILLs its tracees.

Still open

  • No payload inspection. read, write and writev are not trapped, so grith judges what is touched and not what is transferred.
  • The capability filter ships permissive. It enforces only once grants are declared.
  • The mount/ownership and namespace coverage categories are off at defaults. chown, mount, unshare and setns are not evaluated unless you turn them on. Cross-process ptrace coverage, which shares the category-2 name, is on.
  • PTY ownership is audit-only unless pty_ownership_enforce = true.
  • Five delegating-binary residuals remain, listed on Limitations.

The invariant it all rests on

Sticky session containment has no TTL - once a high-taint access arms it, it lasts until the session ends. That guarantee is only worth something if a supervised tool cannot start a fresh session for itself, so the invariant is: session lifecycle is user-initiated only.

  • grith exec runs the supervisor loop exactly once and propagates the result to its exit code. There is no auto-retry path in the CLI or the supervisor.
  • The built-in agent loop retries on context overflow by shrinking the prompt inside the same session. It never allocates a new scope.
  • The dashboard can start fresh sessions, but that endpoint is loopback-bound and token-gated, and it is an operator action.
  • SIGTERM and SIGINT arriving at the supervisor are logged at high severity as shutdown_signal_received, so the interesting case - a supervised tool signalling its own supervisor - leaves a trail.

The consequence: an attacker working through the model cannot wait out containment or reset it, without your cooperation. The one thing that does reset it is a CI wrapper that retries grith exec on failure. If your environment does that, know that each retry is a new session with a clean scope.

See also

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