Skip to main content

In your editor

CI is the last place a policy should fail. This page is about the loop before that — running Tirith on your own machine, while the code is still being written.

It matters most when an agent is drafting the policy for you. Generated policy JSON is plausible by construction: it parses, it looks right, and a policy that matches nothing is indistinguishable from one that works until you evaluate it.

The loop​

tirith fmt .tirith/policies                                                   # layout
tirith lint .tirith/policies # shape
tirith -policy-path .tirith/policies -input-path plan.json --fail-on-error # meaning

Linting checks the shape: that every condition type exists, that no provider_args key belongs to a different provider, that eval_expression names every evaluator, that error_tolerance is inside condition where the engine reads it. Only evaluation checks the meaning. Lint and format covers both commands in full.

Run both against a document that should fail. A guardrail only ever seen passing is a guardrail nobody has tested.

VS Code tasks​

Drop this in .vscode/tasks.json and the loop becomes one keystroke:

.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Tirith: format policies",
"type": "shell",
"command": "tirith fmt .tirith/policies",
"problemMatcher": [],
"group": "test"
},
{
"label": "Tirith: lint policies",
"type": "shell",
"command": "tirith lint .tirith/policies",
"problemMatcher": [],
"group": "test"
},
{
"label": "Tirith: check the plan",
"type": "shell",
"command": "tirith -policy-path .tirith/policies -input-path plan.json --fail-on-error",
"problemMatcher": [],
"group": {"kind": "test", "isDefault": true},
"dependsOn": ["Tirith: lint policies"]
}
]
}

Tirith: check the plan is the default test task, so ⇧⌘B / Ctrl+Shift+B runs the whole loop: lint first, then evaluate, because dependsOn will not run the second if the first exits non-zero.

Catch it at commit time​

Tirith publishes both commands as pre-commit hooks:

.pre-commit-config.yaml
repos:
- repo: https://github.com/StackGuardian/tirith
rev: 1.2.1 # the first tag that publishes the hooks
hooks:
- id: tirith-lint
- id: tirith-fmt
pre-commit install

Both run only on the policy files a commit actually touches, need no network, and exit 3 on a finding. See CI integration for why they lint rather than evaluate, and lint and format for the flags and the files they read.

When an agent is writing the policy​

Install the Tirith skill and your agent gets the closed condition list, the argument key each provider reads, and the instruction to run a policy before claiming it works:

curl -fsSL https://stackguardian.github.io/tirith/skill.sh | sh

Add --cursor for the Cursor rule. Agent Skills covers what is in the pack, the other clients, and how to tell whether it took effect.

Two things make the difference between a drafted policy and a working one:

  1. The agent has tirith on PATH. It is an ordinary command, so an agent with a shell can lint and evaluate its own work without any protocol server or plugin.
  2. You give it a document that should fail. Ask for the policy and a plan that violates it, then check that the exit code is 3. If it is 0, the policy matched nothing.

Reading a failure without leaving the terminal​

tirith --json -policy-path .tirith/policies -input-path plan.json > result.json
tirith ui --result result.json # needs the optional [tui] extra

The explorer names the resource behind each finding — its address, the planned action, and the attributes that changed — which the pretty printer does not show. See the interactive interface.