Detection Rules

208 detection rules across 29 categories. All rules run at every tier, with no features gated behind paid plans.

Every Rule ID below is a stable identifier from the engine. Use it verbatim in policy severity_overrides, allowlist_rules, or tirith explain --rule <id>.

Supply Chain & Ecosystem32

Git, Docker, pip, npm, registries — package and supply-chain attack surface

Rule IDSeverityDescription
git_typosquatMEDIUMA git clone URL targets a repository whose owner or name is within Levenshtein distance 1 of a known popular repository on GitHub, GitLab, or Bitbucket.
docker_untrusted_registryMEDIUMA Docker or Podman command pulls an image from a registry not in the trusted set: docker.io, ghcr.io, gcr.io, quay.io, registry.k8s.io, mcr.microsoft.com, public.ecr.aws.
pip_url_installMEDIUMA pip install command references a URL that does not point to the official PyPI registry.
npm_url_installMEDIUMAn npm install command references a URL that does not point to registry.npmjs.org.
web3_rpc_endpointLOWThe URL matches a known Web3 JSON-RPC provider (Infura, Alchemy, Moralis, QuickNode, etc.) with RPC path patterns.
web3_address_in_urlLOWThe URL contains an Ethereum address pattern (0x followed by 40 hex characters).
vet_not_configuredLOWA cargo install or cargo add command runs without a supply-chain/config.toml present in the current directory. The cargo-vet tool provides supply-chain auditing for Rust dependencies.
repo_add_from_pipeHIGHA network download (curl/wget) is piped through `tee` into an apt sources.list file, or redirected directly into one. The repository definition is added without review and without verifying its signing key.
unsigned_repo_trustHIGHAn apt sources entry uses `[trusted=yes]`, or an apt command passes `--allow-unauthenticated` / `--allow-insecure-repositories`. Either disables GPG signature verification, so packages are installed without an authenticity check.
gpg_check_disabledHIGHA dnf/yum/zypper command passes `--nogpgcheck`, a yum/dnf repo config sets `gpgcheck=0`, or a pacman config sets `SigLevel = Never`. All disable package signature verification.
kubectl_apply_remoteHIGHA `kubectl apply`/`create`/`replace` command is given `-f` with a remote URL. The manifest is fetched and applied to the cluster without local review; shortened URLs additionally hide the real source.
helm_untrusted_repoMEDIUMA `helm install`/`upgrade`/`repo add`/`pull` command references a chart repository URL that is not in the set of recognized chart repositories.
terraform_remote_moduleMEDIUMA `terraform init -from-module` (or `tofu`) command copies a root module from a remote source — a git URL, an http(s) archive, or a code-hosting path — rather than a local directory or the Terraform Registry.
brew_untrusted_tapHIGHA `brew install` command is given a raw URL instead of a formula name, or a `brew tap` is pointed at an explicit non-GitHub git remote. Homebrew formulae are executable Ruby.
workflow_unpinned_actionMEDIUMA GitHub Actions workflow `uses:` a third-party action pinned to a mutable ref — a branch (`@main`) or a tag (`@v3`) — rather than an immutable 40-character commit SHA. Tags and branches can be re-pointed by whoever controls the action repository.
workflow_dangerous_triggerHIGHA GitHub Actions workflow is triggered by `pull_request_target`. Unlike `pull_request`, this trigger grants the workflow repository secrets and a privileged token even for pull requests from forks.
workflow_curl_pipe_shellHIGHA workflow `run:` step fetches a script over the network (curl/wget) and pipes it directly into a shell interpreter. The fetched code is whatever the server returns at run time.
workflow_untrusted_inputHIGHA workflow `run:` step interpolates a `${{ github.event.* }}` (or `github.head_ref`) expression — a PR title, issue body, branch name, comment, … — directly into a shell command. Those values come from untrusted contributors.
dockerfile_unpinned_imageMEDIUMA Dockerfile `FROM` instruction uses a base image on the mutable `latest` tag, or with no tag at all (Docker defaults to `latest`), and with no `@sha256:` digest pin.
package_script_dangerousHIGHA `package.json` preinstall, install, or postinstall lifecycle script runs a dangerous command — a pipe-to-shell, a base64-decode-and-execute payload, an inline interpreter one-liner that shells out or opens a network connection, or a download-and-run sequence.
package_not_found_in_registryMEDIUMThe package name was resolved against its registry's public API and the API responded HTTP 404 — the registry has no such package. This is distinct from `ApiSignals::Unavailable` (the registry could not be reached, or there is no adapter), which is honest no-signal rather than a positive `not found` claim. A package that does not exist in the registry cannot be the package the operator intended; the install will fail at the package-manager step OR will resolve to a same-named package on a different (possibly attacker-controlled) registry.
package_maintainer_change_recentMEDIUMA diff between the two most recent registry snapshots tirith has on disk for this package shows that the maintainer set changed within the recency window. This is recorded snapshot-vs-snapshot data — it captures real history that a single registry-API response cannot, since a single response carries the *current* maintainer set with no record of who held it last week. tirith writes one snapshot per successful `--online` fetch (reusing the existing API response — no extra request).
package_ownership_transferredMEDIUMTwo registry snapshots tirith recorded for this package show every previous maintainer is gone and the new maintainer set is non-empty — the detectable shape of a full ownership transfer. This is a stronger signal than the legacy `ApiProvenance::ownership_transferred` flag (which only fires when the *current* maintainer list is empty), because it correlates two points in time and proves the change rather than inferring it from absence-of-data.
package_osv_advisory_activeHIGHThe package@version pair matched one or more advisories in the OSV.dev database via the shipping runtime threat-intel cache (`threatdb_api.rs`). The version-aware `<name>[@<version>]` CLI parsing routes the version through to the cache lookup — bare `<name>` cannot correlate to a version-pinned advisory.
package_dependency_confusionHIGHtirith fetched a package from the public registry whose name matches a pattern the policy declares is for internal use only. Sources: (a) the operator-supplied `package_policy.internal_package_names` policy list (M6 ch7 schema location); (b) registry-namespace shape — an `@<reserved-org>/<name>` npm package resolved on the public registry. Without an operator-supplied internal-names list, only the obvious public-namespace shape trips this.
package_install_script_network_callMEDIUMtirith's install-script analysis found a network-call pattern (curl/wget/fetch/http.get/...) inside an npm lifecycle script, a `setup.py` body, or a `build.rs` body. The analysis is read-only and operates on text already on disk OR text embedded in the registry-API response — it NEVER executes the script and NEVER downloads a package. Per-ecosystem scope: npm registry response carries `scripts.{...}` inline; PyPI METADATA does NOT carry `setup.py`; crates.io does NOT carry `build.rs` — those two need the installed-tree path (script text on disk under `node_modules` / `site-packages` / source extraction).
package_repo_mismatchHIGHUnder `--online` only: tirith fetched the package's registry-claimed repository URL and verified (a) the URL is reachable (HTTP 200), (b) it parses as a known git-hosting URL, and (c) the hosted repository's manifest mentions this package name. A mismatch — dead URL, unparseable URL, or hosted manifest naming a different package — emits this rule. Offline default state is `Unverifiable` and no finding is emitted.
package_policy_newer_than_daysHIGHThe policy's `package_policy.warn_newer_than_days` or `block_newer_than_days` crosses the registry-reported `package_age_days`. Requires `--online` to gather the age signal — offline runs cannot fire this rule. Block fires when both thresholds are configured and the age is at or below the Block threshold; otherwise Warn.
package_policy_low_downloadsMEDIUMThe policy's `package_policy.warn_low_downloads_below` is at or above the registry-reported `recent_downloads` count. Requires `--online` to gather the downloads signal — offline runs cannot fire this rule. Warn only; there is no Block elevation path (low downloads alone is too weak).
package_policy_typosquat_distanceHIGHThe `name_vs_popular` classification is `NearPopular` AND the resolved edit distance is at or below the policy's `block_typosquat_distance`. Block. The threat DB must be installed for `NearPopular` to fire — see `threat-db update`.
package_policy_unknown_package_with_install_scriptsHIGHThe package's `name_vs_popular` classification is `Unknown` AND its install-script analysis flagged either `has_network_call` or `has_shell_spawn`. Block. Requires the install-script signal to be available; offline `tirith install` cannot evaluate this and prints '(install-script signal not available offline — pass `--online` to evaluate)'.
package_policy_not_foundHIGHRequires `--online`: the registry positively responded HTTP 404 for `(ecosystem, name)`. `PackageExistence::Unknown` (timeout / unsupported registry / offline) does not fire this rule — we never block on absence-of-data, only on a positive 404. The ch6 sibling `package_not_found_in_registry` fires Medium-baseline on the same evidence; this rule adds the policy-driven Block elevation.

Command Execution21

Pipe-to-shell, decode-execute, command substitution, and dangerous invocations

Rule IDSeverityDescription
pipe_to_interpreterHIGHA command pipes output directly to a shell interpreter (bash, sh, zsh, python, node, etc.). The piped content executes immediately without the user seeing it.
curl_pipe_shellHIGHA curl command pipes downloaded content directly to a shell interpreter. The remote server controls what executes on your machine.
wget_pipe_shellHIGHA wget command pipes downloaded content directly to a shell interpreter.
httpie_pipe_shellHIGHAn HTTPie command (http or https CLI tool) pipes downloaded content to a shell interpreter.
xh_pipe_shellHIGHAn xh command (HTTPie-compatible HTTP client) pipes downloaded content to a shell interpreter.
dotfile_overwriteHIGHA command redirects output to a dotfile in the home directory (> ~/.bashrc, >> $HOME/.profile, etc.). This can inject persistent shell hooks.
archive_extractMEDIUMA tar, unzip, or 7z command extracts to the root directory, home directory, or $HOME. Archives may contain path traversal entries that overwrite critical files.
proc_mem_accessHIGHA command reads from /proc/<pid>/mem or /proc/self/mem, which provides direct access to process memory. This is a common credential-harvesting technique.
docker_remote_priv_escCRITICALA Docker or Podman command connects to a remote daemon via tcp:// and uses --privileged or mounts the host root filesystem (--volume /:/). This combination grants full root access to the remote host.
credential_file_sweepMEDIUMA single command accesses two or more credential file paths (.ssh/id_*, .aws/credentials, .kube/config, .docker/config.json, .npmrc, .pypirc, .netrc, .gnupg/, .git-credentials, etc.).
base64_decode_executeHIGHA command decodes base64 content and pipes it to execution: base64 -d | bash, PowerShell -EncodedCommand, Buffer.from() in Node, or atob() chains.
data_exfiltrationHIGHA curl or wget command uses upload flags (-T, --data-binary, --data-urlencode, --upload-file) pointing to an external host. This pattern is used to exfiltrate data.
wrapper_chain_too_deepMEDIUMA command pipes output into an interpreter hidden behind more wrapper layers (sudo, env -S, command, exec, nohup) than tirith's interpreter resolver will unwrap. Because the real sink interpreter cannot be resolved, the curl/wget/pipe-to-shell rules cannot fire — this rule closes that silent-evasion gap by surfacing the obfuscation itself.
ps_set_execution_policy_bypassHIGHA PowerShell command sets the script execution policy to Bypass — either via the Set-ExecutionPolicy cmdlet, or via the powershell.exe -ExecutionPolicy Bypass flag (also accepts the documented short alias -ep). Any form removes script signing enforcement for the affected scope.
ps_defender_exclusionHIGHA PowerShell command calls Add-MpPreference with -ExclusionPath, -ExclusionProcess, or -ExclusionExtension, hiding files, processes, or file extensions from Windows Defender scans.
ps_inline_download_executeHIGHA PowerShell command uses iex / invoke-expression as the leading command with at least one URL anywhere in the command or its arguments, e.g. iex (iwr https://example.com/install.ps1). The pipe form (iwr url | iex) is handled by pipe_to_interpreter.
sudo_shell_spawnHIGHA `sudo` invocation whose inner command is an interactive shell (`sh`, `bash`, `zsh`, `fish`, `dash`, `ksh`, `tcsh`, `csh`, `ash`, `mksh`, `pwsh`, `powershell`, `nu`). Subsequent commands typed into the spawned root shell run with full privileges and are NOT intercepted by tirith.
sudo_env_preserve_sensitiveHIGHA `sudo -E` / `--preserve-env` invocation where at least one sensitive env var from `assets/data/sensitive_env.toml` is currently set in the parent shell. The targeted form `--preserve-env=VAR_LIST` also fires when the listed names intersect the sensitive set (presence-only — tirith does not read the values).
sudo_tee_system_fileHIGHA `sudo tee <path>` invocation where the target path lives under `/etc/…`, `/usr/local/bin/…`, `/usr/bin/…`, `/usr/sbin/…`, `/lib/…`, or matches `/etc/cron*` / `/etc/systemd/*`. Targets under `/tmp`, `/var/tmp`, `~`, `$HOME`, and repo-relative paths never fire.
sudo_download_installHIGHA `sudo curl|wget|fetch -o <path>` (or `--output=…` / `-O …`) invocation where the destination path is under a protected system directory (`/etc/`, `/usr/local/bin/`, `/usr/bin/`, `/usr/sbin/`, `/lib/`, `/etc/cron*`, `/etc/systemd/*`). Downloads to `/tmp`, `~`, `$HOME`, or repo-relative paths never fire.
sudo_recursive_perms_broad_pathHIGHA `sudo chmod|chown -R …` invocation where the trailing positional path is one of `/`, `/home`, `/usr`, `/etc` (with or without a trailing slash). Narrower paths (`/etc/myapp`, `/home/me/project`, `/usr/local/bin/myapp`) never fire — the rule is intentionally narrow to keep false-positives low.

Contextual Safety14

Production cloud/k8s, labeled SSH hosts, IaC apply/destroy, and container exec

Rule IDSeverityDescription
context_prod_destructive_commandHIGHThe active provider context (kube current-context / AWS_PROFILE / gcloud config / az account) is labeled `critical` or `production` and the command's verb is destructive.
context_prod_write_operationMEDIUMThe active provider context is labeled `critical` or `production` and the command's verb mutates state without deleting it (apply / patch / upgrade / start / stop / cp into a prod prefix).
context_prod_credential_changeHIGHThe active provider context is labeled `critical` or `production` and the command mutates IAM / RBAC / access-key state.
ssh_remote_destructive_on_labeled_hostHIGHAn `ssh host '<cmd>'` invocation targets a host listed in the SSH host-labels file with criticality `critical` / `production` / `prod` / `live` / `p0` / `p1`, and the inner command's verb is destructive (e.g. `sudo systemctl restart`, `rm -rf`, `dd`), a write mutation (`systemctl reload`, `iptables`), or a credential change (`passwd`, `useradd`).
ssh_remote_shell_on_labeled_hostINFOA bare `ssh host` (no inner command) was issued against a host whose label is `critical` / `production` / `prod` / `live` / `p0` / `p1`. The finding is a reminder, not a block.
iac_apply_without_planHIGHA `terraform apply` / `pulumi up` / `tofu apply` invocation has no positional plan-file argument, and `policy.iac_require_plan_before_apply` is on. Run `terraform plan -out tfplan && tirith iac check-plan tfplan && terraform apply tfplan` instead.
iac_apply_auto_approveMEDIUMA `terraform apply -auto-approve`, `pulumi up --yes`, or `tofu apply -auto-approve` invocation was issued and the active provider context is not labeled `critical` / `production`. CI workflows commonly run with these flags — see `false_positive_guidance`.
iac_apply_auto_approve_prodHIGHA `terraform apply -auto-approve` (or `pulumi up --yes`, `tofu apply -auto-approve`) invocation was issued and the active provider context is labeled `critical` / `production` / `prod` / `live` / `p0` / `p1`. Requires both the M8 ch1 context detection and an operator-labeled context.
iac_destroy_prodHIGHA `terraform destroy` / `pulumi destroy` / `tofu destroy` invocation was issued and the active provider context is labeled `critical` / `production`. The rule requires both the M8 ch1 context detection and an operator-labeled context.
iac_plan_high_risk_changesMEDIUMThe `tirith iac check-plan <tfplan>` command parsed the saved plan's JSON (via `terraform show -json` or `pulumi preview --json`) and found at least one resource change that matches the high-risk categories: `aws_iam_*`, `*_security_group*`, `aws_s3_bucket_public_access_block`, `*_db_instance` deletes, `*_load_balancer*`. Surfaces the signal so the operator can review before applying.
iac_plan_hash_mismatchHIGHA `terraform apply <tfplan>` (or `tofu apply <tfplan>`) invocation was issued, `policy.iac_require_plan_before_apply` is on, and the SHA-256 hash of `<tfplan>` does not match any plan recorded in `state_dir()/iac_plans/`.
docker_run_privilegedHIGHA `docker run` / `docker create` / `podman run` invocation whose argument list contains `--privileged` (or `--privileged=true`). Also fires when scanning a `.devcontainer/devcontainer.json` whose `runArgs` array contains `--privileged`.
docker_run_sensitive_bind_mountHIGHA `docker run` (or `docker create`, `podman run`) whose `-v src:dst[:opts]` / `--volume src:dst` / `--mount type=bind,source=src,…` argument names one of the documented sensitive source paths. Also fires when scanning a `.devcontainer/devcontainer.json` whose `runArgs` or `mounts` arrays expose a sensitive source.
docker_exec_prod_containerMEDIUMA `docker exec <container> …` invocation where `<container>` has a `container:<name>` entry in `policy.context_labels` resolving to `prod` / `production` / `critical` / `live` / `p0` / `p1`. Fires regardless of the inner command — even a benign `docker exec payments-prod ls` triggers the audit-trail signal.

Config Security12

AI config poisoning, prompt injection, and MCP server validation

Rule IDSeverityDescription
config_injectionHIGHAn AI agent configuration file (.cursorrules, .claude/settings, MCP config, etc.) contains patterns that attempt to override the agent's instructions, bypass permissions, or inject new behavioral directives.
config_suspicious_indicatorMEDIUMA configuration file contains weak but noteworthy patterns: claims of unrestricted access, overly broad permissions, or always-apply directives.
config_malformedMEDIUMA configuration file fails to parse as its expected format (JSON, YAML). This may indicate manual tampering, merge conflicts, or corruption.
config_non_asciiMEDIUMA configuration file that should contain only ASCII has non-ASCII bytes. This may be invisible Unicode used to hide instructions.
config_invisible_unicodeCRITICALA configuration file contains zero-width characters, bidi controls, or Unicode Tag characters. These are invisible and can carry hidden instructions for AI agents.
mcp_insecure_serverHIGHAn MCP (Model Context Protocol) server configuration uses insecure transport (e.g., http:// URL). Tool invocations and responses are transmitted in cleartext.
mcp_untrusted_serverMEDIUMAn MCP server is configured from a source that is not in the known-trusted registry list.
mcp_duplicate_server_nameLOWTwo or more MCP servers are configured with the same name. This can lead to tool shadowing where one server's tool implementation silently replaces another's.
mcp_overly_permissiveMEDIUMAn MCP server is configured with overly broad permissions that grant access to sensitive resources or operations.
mcp_suspicious_argsHIGHAn MCP server's command-line arguments contain shell metacharacters (;, |, &, backtick, $) that could be interpreted as command injection.
mcp_server_driftMEDIUMThe MCP servers declared in this repository's configuration files no longer match the inventory recorded in `.tirith/mcp.lock`. A server may have been added or removed; an existing server may have changed transport (a swapped command, args, or remote URL), gained or lost an env variable, had a stored credential rotated (env value-hash or URL userinfo-hash change), or had its declared tools modified. None of these changes are inherently malicious — they are exactly what a config update looks like — but each is a security-relevant surface change that should be reviewed and the lockfile re-committed. Policy `scan.trusted_mcp_servers` filters drift entries by server name before a finding is built — a trusted name does not raise drift. Policy `scan.mcp_allowed_tools` adds a per-server tool gate: a newly-added tool outside the allowed set escalates the drift to High, and a lockfile that records a tool outside the allowed set surfaces a High-severity finding on its own. The rule also fires when `.tirith/mcp.lock` itself is unparseable (not valid JSON, or not matching the lockfile schema) — without a loadable baseline the scan cannot diff the current inventory at all, which is exactly the failure mode an attacker would use to hide an MCP-surface change behind a deliberately broken lockfile.
agent_instruction_hiddenHIGHAn AI agent-instruction file (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, and similar) contains hidden content — a directive inside an HTML comment (`<!-- … -->`, which Markdown renders to nothing), or a visually-hidden HTML element (`hidden`, `aria-hidden`, `display:none`). These files legitimately contain *visible* instructions, so only *hidden* directives are flagged.

Threat Intelligence12

Known-malicious packages, URLs, and IPs from the signed threat database

Rule IDSeverityDescription
threat_malicious_packageCRITICALThe package being installed is listed in the tirith threat intelligence database as known-malicious. Sources include the OpenSSF Malicious Packages dataset and the Datadog Malicious Software Packages dataset. The package may contain backdoors, data exfiltration, cryptocurrency miners, or other malware.
threat_malicious_ipHIGHA command references an IP address that appears in the tirith threat intelligence database as a known command-and-control server, botnet node, or other malicious infrastructure. Source: Feodo Tracker IP blocklist.
threat_package_typosquatHIGHThe package being installed is listed in the ecosyste.ms confirmed typosquatting dataset. This means the package name was identified as a deliberate impersonation of a legitimate, popular package. Typosquats are a common supply chain attack vector.
threat_package_similar_nameMEDIUMThe package being installed has a name that differs by only one character from a well-known, popular package. This pattern is commonly used in typosquatting attacks but may also be a coincidence. The finding is advisory — verify the package name is intentional.
threat_malicious_urlHIGHA URL in the command references a hostname known to distribute malware. This rule activates when optional keyed threat feeds (URLhaus) are configured. Phase B feature — requires feed configuration.
threat_phishing_urlHIGHA URL in the command references a domain known for phishing attacks. This rule activates when optional keyed threat feeds (PhishingArmy, PhishTank) are configured. Phase B feature — requires feed configuration.
threat_tor_exit_nodeMEDIUMA command references an IP address that is a known Tor exit node. While Tor has legitimate privacy uses, connections to Tor exit nodes in automated commands may indicate traffic anonymization for malicious purposes. Phase B feature — requires feed configuration.
threat_threat_fox_iocHIGHA command references an indicator of compromise (domain, IP, or URL) that is listed in the ThreatFox IOC database. This rule activates when optional keyed threat feeds are configured. Phase B feature — requires feed configuration.
threat_osv_vulnerableHIGHThe package being installed has a known vulnerability or malware entry in the Open Source Vulnerabilities (OSV.dev) database. This rule uses real-time API lookup for live vulnerability data. Phase C feature — requires network access.
threat_cisa_kevHIGHThe package being installed is associated with a CVE listed in CISA's Known Exploited Vulnerabilities (KEV) catalog. These vulnerabilities are confirmed to be actively exploited. Phase C feature — requires runtime OSV.dev lookup for CVE-to-package correlation.
threat_suspicious_packageLOWThe package has characteristics often associated with malicious packages. Two paths emit this rule. (1) Real-time API lookups (deps.dev, ecosyste.ms) flag a very recently published package, a low download count, or a missing repository URL. (2) `tirith ecosystem scan` flags a declared dependency whose name is slopsquat-suspicious — not a known-real package, name-shaped like an LLM hallucination (a language prefix plus descriptive tokens, or a stack of generic filler words), and sitting near a real popular package. Slopsquatting is the registration of plausible-but-fake names that AI assistants tend to hallucinate when suggesting dependencies.
threat_safe_browsingHIGHA URL in the command is flagged by Google Safe Browsing as hosting malware, phishing content, or unwanted software. This rule requires the user to configure their own Google Safe Browsing API key. Phase C feature — requires API key configuration.

Terminal Injection11

ANSI escapes, bidi overrides, zero-width and control characters

Rule IDSeverityDescription
ansi_escapesHIGHPasted content contains ANSI escape sequences (ESC + [) that can manipulate the terminal display, cursor position, or window title.
control_charsHIGHThe input contains control characters that overwrite displayed content: CR not followed by LF (display-overwriting), backspace, BEL, VT, FF, DEL, or OSC/APC/DCS escape sequences.
bidi_controlsCRITICALThe input contains Unicode bidirectional control characters (U+202A-E, U+2066-2069, U+200E/F) that change text display direction. Code appears to read one way but executes differently.
zero_width_charsCRITICALThe input contains zero-width characters (ZWSP, ZWJ, ZWNJ, CGJ, soft hyphen, word joiner, Mongolian vowel separator, or misplaced BOM). These are invisible but may alter parsing. ZWJ/ZWNJ are suppressed in joining-script contexts where they serve a legitimate purpose.
hidden_multilineHIGHA multiline paste contains shell command patterns (curl, bash, sudo, rm, etc.) on non-first lines. Users often only read the first line of pasted content.
unicode_tagsCRITICALThe input contains Unicode Tag characters that encode hidden ASCII. Each tag character maps to an ASCII character (U+E0041 = A), creating invisible text that can carry hidden instructions.
invisible_math_operatorMEDIUMThe input contains invisible Unicode mathematical operators: function application (U+2061), invisible times (U+2062), invisible separator (U+2063), or invisible plus (U+2064).
variation_selectorMEDIUMThe input contains Unicode variation selectors (VS1-VS256). While commonly used in emoji, they can encode hidden data steganographically.
invisible_whitespaceMEDIUMThe input contains unusual Unicode whitespace variants: en space, em space, figure space, medium mathematical space, etc. These are visually indistinguishable from regular spaces.
hangul_fillerMEDIUMThe input contains Hangul filler characters that are invisible in most terminal fonts. These can be used to embed hidden text.
confusable_textHIGHThe input contains Unicode characters that look identical to ASCII: mathematical alphanumerics (U+1D400-U+1D7FF) near ASCII text, or Cyrillic/Greek confusables mixed into the same word as ASCII characters.

Hostname & Homograph10

Homograph attacks, punycode, confusable characters, IDN spoofing

Rule IDSeverityDescription
non_ascii_hostnameHIGHThe hostname contains characters outside the ASCII range (bytes > 0x7F). Attackers use visually similar Unicode characters to impersonate legitimate domains.
punycode_domainHIGHThe hostname contains a punycode-encoded label (xn-- prefix), which is the ASCII-compatible encoding of an internationalized domain name. The decoded characters may impersonate a trusted domain.
mixed_script_in_labelHIGHA single DNS label in the hostname contains characters from multiple Unicode scripts (e.g., Latin mixed with Cyrillic). Legitimate domains almost never mix scripts within a label.
userinfo_trickHIGHThe URL contains a userinfo component with a dot (e.g., http://[email protected]/). Browsers and tools resolve the actual host from after the @, but users see the fake domain first.
confusable_domainHIGHThe domain matches the skeleton of a known high-value domain after Unicode confusable normalization, OCR confusion mapping (e.g., rn resembles m), or is within edit distance 1 of a known domain.
raw_ip_urlMEDIUMThe URL uses a raw IPv4 or IPv6 address instead of a hostname. Loopback addresses (127.x, ::1) are exempt.
non_standard_portMEDIUMA URL to a known high-value domain uses a port outside the standard set (80, 443, 22, 9418). This is unusual for legitimate services.
invalid_host_charsHIGHThe hostname contains characters that are invalid in DNS labels: percent signs, backslashes, ASCII control characters, whitespace, or full-width dots (U+FF0E).
trailing_dot_whitespaceMEDIUMThe hostname ends with a dot or whitespace character, which may cause parser disagreements about the actual target host.
lookalike_tldMEDIUMThe URL uses a TLD that resembles a common file extension (.zip, .mov, .app, .dev, .run). Attackers register these domains to trick users into thinking a URL is a file download.

Security Hygiene10

Loose permissions, exposed secrets, and risky file/credential handling

Rule IDSeverityDescription
hygiene_private_key_loose_permsHIGHA `~/.ssh/id_*` private key (a file named `id_*` without a `.pub` suffix) has permission bits granting group or other any access (`mode & 0o077 != 0`). The comparison is mask-based, so a key at 0600 passes regardless of file-type / sticky bits across macOS and Linux.
hygiene_env_world_readableHIGHA `.env` / `.env.*` / `*.env` file in the repo tree has the other-read bit set (`mode & 0o004 != 0`). The repo walk skips `.git`, `node_modules`, `target`, and `vendor`.
hygiene_kubeconfig_group_readableMEDIUM`~/.kube/config` has permission bits granting group or other any access (`mode & 0o077 != 0`).
hygiene_npmrc_plaintext_tokenHIGH`~/.npmrc` contains an `_authToken` / `_password` / `_auth` assignment whose value is a literal (not an `${ENV}` reference).
hygiene_pypirc_plaintext_tokenHIGH`~/.pypirc` contains a `password` assignment with a literal value, or a literal `pypi-…` API token in a value position.
hygiene_ssh_config_unsafe_includeMEDIUM`~/.ssh/config` contains an `Include` directive whose target is an absolute path outside `~/.ssh`, a `~/…` path outside `.ssh`, or a relative path that climbs out with `../`.
hygiene_git_credential_helper_storeMEDIUM`~/.gitconfig` sets `credential.helper = store`, either as a one-line `credential.helper = store` or as `helper = store` inside a `[credential]` section.
hygiene_shell_history_secret_likeMEDIUMA shell history (`~/.bash_history`, `~/.zsh_history`, `~/.history`, fish history) contains text matching tirith's SHIPPING high-confidence credential detector (`rules::credential`, the same provider-pattern + entropy gate the paste interceptor uses). No new regex is introduced for history scanning to keep false positives low.
hygiene_cloud_creds_bad_permsHIGH`~/.aws/credentials` (or `~/.aws/config`) has permission bits granting group or other any access (`mode & 0o077 != 0`).
hygiene_db_dump_in_repoMEDIUMA `*.dump` or `*.sql` file exists in the repo tree (the walk skips `.git`, `node_modules`, `target`, `vendor`, and dot-directories).

Code Execution10

Executing from tmp/untrusted locations and dynamic or obfuscated execution

Rule IDSeverityDescription
exec_in_tmpMEDIUMThe engine (with `exec_guard_enabled`) resolved the command leader to a path under /tmp or $TMPDIR using a stat-free string compare. No stat / signature / file-type probe runs on this hot-path check — run `tirith exec check` for full provenance.
exec_recently_modifiedHIGH`tirith exec check|provenance` stat'd the resolved executable and found its modification time within the 5-minute window. NEVER runs on the engine hot path (it requires a stat).
exec_world_writableHIGH`tirith exec check|provenance` found the world-write bit set on the resolved executable. NEVER runs on the engine hot path.
exec_shadows_system_commandMEDIUM`tirith exec check` / `tirith path which` resolved the full $PATH and found the command resolves to a non-system path while a same-named binary exists in a system dir. NEVER runs on the engine hot path.
exec_unsignedMEDIUM`tirith exec check|provenance` ran `codesign --verify --strict` (macOS) with a 2-second timeout and it failed. On Linux/Windows the check reports not-applicable and this rule does not fire. NEVER runs on the engine hot path.
exec_in_repo_binMEDIUMThe engine (with `exec_guard_enabled`) resolved the command leader to a path inside the repo root via a stat-free prefix compare. No stat / signature / file-type probe runs here. Run `tirith exec check` for full provenance.
path_writable_dir_before_systemHIGHThe engine (with `exec_guard_enabled`) resolved the command leader from a directory that (a) appears in $PATH before a system dir, (b) is repo-local or under /tmp, and (c) is writable by the current user. The broader 'any writable dir before system' inventory is surfaced (informationally) by `tirith path audit`.
path_duplicate_command_nameMEDIUM`tirith path audit` enumerated executables across $PATH dirs and found a name resolving in more than one. The reported entry points at the SHADOWED (later) copy. NEVER runs on the engine hot path.
path_dir_in_repoMEDIUM`tirith path audit` found a $PATH directory inside the repo root. NEVER runs on the engine hot path.
path_dir_in_tmpHIGH`tirith path audit` found a $PATH directory under /tmp or $TMPDIR. NEVER runs on the engine hot path.

Hidden Content9

Hidden CSS/color text, comments, and notebook/HTML hidden instructions

Rule IDSeverityDescription
notebook_hidden_contentHIGHA Jupyter notebook (`.ipynb`) cell contains content hidden from the rendered view — invisible / bidirectional / zero-width characters in the cell source, a long base64-encoded blob embedded in the source, or a cell marked hidden via `metadata.jupyter.source_hidden` or a `hide_input` tag.
notebook_suspicious_outputHIGHA stored cell output in a Jupyter notebook (`.ipynb`) carries hidden or active content — invisible / bidirectional / zero-width characters, an embedded `<script>` element, an inline event handler, a `javascript:` URI, a saved JavaScript (`application/javascript` / `text/javascript`) output, or content hidden via CSS — inside a saved `text/html`, JavaScript, or stream output.
svg_script_embeddedHIGHAn SVG file contains executable content — an embedded `<script>` element, an inline `on*` event-handler attribute, or a `javascript:` URI. A static SVG image (paths, shapes, gradients, text) needs none of these.
svg_external_referenceMEDIUMAn SVG file references content outside itself — a remote `xlink:href` / `href` (http(s) or protocol-relative), or an XXE external-entity declaration (`<!ENTITY … SYSTEM "…">`). A self-contained SVG image references nothing external.
hidden_css_contentHIGHHTML content uses CSS properties to hide text: display:none, visibility:hidden, opacity:0, font-size:0, clip:rect(0...), or off-screen positioning (-9999px).
hidden_color_contentHIGHHTML content has text with the same color as its background (white-on-white, etc.), making it invisible to humans but readable by text processors.
hidden_html_attributeHIGHHTML content uses the hidden attribute, aria-hidden=true, or similar attributes to hide elements from rendering. Hidden content may contain instructions for AI agents.
markdown_commentINFOA Markdown file contains HTML comments. These are invisible in rendered output but may contain hidden instructions for AI agents.
html_commentINFOAn HTML file contains comments that are invisible in rendered output.

Terminal Output9

Malicious escape sequences in program output (OSC52, title rewrite, clear)

Rule IDSeverityDescription
output_osc52_clipboard_writeHIGHAn OSC 52 escape sequence appeared in the scanned output. `\e]52;<clipboard>;<base64>\a` is the xterm/iTerm/kitty clipboard-write protocol — content silently lands in the user's system clipboard. `tirith view` strips the sequence before printing; sanitized output is safe to display.
output_hidden_textMEDIUMOutput contains text the user cannot see but downstream consumers (an AI agent reading the stream, a `grep`-on-log pipeline, a copy-paste into a chat client) still process. v1 detection is deliberately narrow: (i) explicit ANSI foreground == explicit ANSI background within the same SGR sequence (no inference of the user's terminal default colors), OR (ii) a zero-width-character run longer than 8 chars. Theme-dependent hiding (text inherits the user's default color) is out of v1.
output_fake_promptMEDIUMOutput ends with (or contains mid-stream) a line shaped like a typical shell prompt: a user@host fragment, a `$ ` / `# ` / `> ` trailer, or a `PS1`-like pattern with no preceding newline that breaks the illusion. Used by attackers running a fake interactive program that captures the user's next command.
output_terminal_hyperlink_mismatchHIGHAn OSC 8 terminal hyperlink was found where the visible link text itself parses as a URL whose host differs from the hyperlink's `href` host. This is the terminal equivalent of a phishing `<a href=evil.com>github.com</a>`. We only flag when the visible text PARSES as a URL — 'Click here' vs `https://example.com` does not fire (legitimate URL-shortener pattern); `github.com` vs `https://evil.example` does fire.
output_title_manipulationINFOOutput contains an OSC 0 or OSC 2 sequence that rewrites the terminal window title. Legitimate programs (your shell, vim, tmux) DO write the title, but they do it directly to /dev/tty — they don't emit it into a stream you pipe through `tirith view` or stash in a log file.
output_clear_screenINFOOutput contains an explicit ANSI screen-clear (`\e[2J`) or cursor-home (`\e[H`) sequence. Legitimate programs that clear the screen (top, htop, less) do it directly on /dev/tty — these sequences in a piped / saved stream are nearly always trying to hide what came before them.
output_truncated_escape_sequenceMEDIUMOutput ended while the byte-scanner was still inside an OSC (`\e]...`) or CSI (`\e[...`) sequence. The previous implementation silently dropped this state; we now surface it so MCP gateway / `tirith view` callers can fail-closed.
prompt_injection_in_outputHIGHScanned text contains a well-known prompt-injection seed phrase. Sophisticated injections (encoded payloads, paraphrases, cross-language phrasing) will slip past this rule — treat every line of agent output as untrusted regardless of whether the rule fires.
ignore_previous_instructionsHIGHScanned text contains an explicit instruction-override phrase. As with the broader `prompt_injection_in_output` rule, this catches well-known seed text only — encoded or paraphrased versions will not fire.

Environment8

Proxy hijacking, sensitive exports, and env-based code/shell injection

Rule IDSeverityDescription
proxy_env_setLOWThe command sets HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, or similar proxy environment variables. Traffic may be intercepted by the proxy.
sensitive_env_exportHIGHThe command exports sensitive credentials (AWS_ACCESS_KEY_ID, GITHUB_TOKEN, OPENAI_API_KEY, etc.) via export, env, or set. These values persist in shell history and are visible to child processes.
code_injection_envCRITICALThe command sets a dynamic linker environment variable (LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH) that causes arbitrary shared libraries to be loaded into every executed process.
interpreter_hijack_envHIGHThe command sets an interpreter module search path variable that causes the interpreter to load modules from attacker-controlled locations.
shell_injection_envCRITICALThe command sets a shell startup environment variable (BASH_ENV, ENV, PROMPT_COMMAND) that causes arbitrary code execution whenever a shell starts or a prompt is displayed.
env_sensitive_exposed_to_unknown_scriptHIGH`engine::analyze` (with `env_guard_enabled`) found one or more sensitive env vars set while the command pipes a network fetch into a shell. The finding lists the exposed variable NAMES only — never the values. This is the dedicated rule the `--suggest-safe-command` env-scrub rewrite attaches to, suggesting `env -u VAR … <cmd>` to strip the secrets for that one invocation.
env_sensitive_persisted_in_shell_rcHIGH`tirith env guard` parsed your rc/profile files and found a `export NAME=…` (or fish `set -x` / PowerShell `$env:`) for a name on the sensitive list. The finding reports the file + line with the value MASKED to `****`; tirith never reads or prints the value.
env_printenv_to_network_sinkMEDIUM`engine::analyze` (with `env_guard_enabled`) found a bare `printenv` or argument-free `env` piped into a network sink. `env FOO=1 some-cmd` (which RUNS a command rather than printing the environment) does NOT fire.

Persistence7

Shell rc, cron, profile, and startup-file modifications for persistence

Rule IDSeverityDescription
persistence_shell_rc_modifiedMEDIUMA watched shell rc/profile file's sha256 changed since the recorded `tirith persistence` snapshot. The diff surfaces only the ADDED lines (credential-redacted), never removed lines or full content.
persistence_authorized_keys_new_entryHIGH`~/.ssh/authorized_keys` gained one or more lines since the recorded snapshot. The diff surfaces the added key line(s) (credential-redacted).
persistence_crontab_modifiedMEDIUMThe output of `crontab -l` changed since the recorded snapshot. Absence of a crontab is treated as empty, not a change. The diff surfaces added cron lines (credential-redacted).
persistence_launch_agent_addedHIGHA launchd plist or systemd-user unit file appeared (or its content changed) since the recorded snapshot. Units are tracked by content hash; the diff surfaces added lines for text units (credential-redacted).
persistence_ssh_config_includeMEDIUM`~/.ssh/config` gained an `Include` directive that was not present in the recorded snapshot. Only an ADDED `Include` line fires this rule — an unrelated `Host`/option edit does not. The diff surfaces the added line.
persistence_direnv_new_envrcMEDIUMA `.envrc` file appeared in the current working directory's ancestry (or its content changed) since the recorded snapshot. The diff surfaces added lines (credential-redacted).
post_run_shell_rc_modifiedHIGHFired by `tirith watch -- "<cmd>"`, which records the sha256 of each shell rc/profile file, runs the command, then re-hashes. A changed hash means the command modified that file during the run. The finding reports WHICH file changed; it does not echo the file contents.

Blast Radius7

Destructive commands with wide reach — mass delete, deletes outside the repo

Rule IDSeverityDescription
blast_deletes_outside_repoHIGHSurfaced ONLY by `tirith preview -- "<cmd>"`, which walks the filesystem to resolve the command's targets. At least one target canonicalizes outside the repo root. The `tirith check` hot path never walks the filesystem and never emits this rule — it is a simulator-only signal.
blast_writes_system_pathHIGHFired on the `engine::analyze` hot path by the CHEAP, filesystem-free `blast_radius::cheap_check`: the parsed target is a well-known system path. No filesystem walk is involved — this is a pure string-shape check. Run `tirith preview` for the full impact count.
blast_symlink_traversalMEDIUMSurfaced ONLY by `tirith preview -- "<cmd>"`, which walks the target tree (links are counted, never followed). The presence of symlinks is reported so you can confirm none of them point somewhere you do not intend to touch. The hot path never walks the filesystem and never emits this rule.
blast_empty_var_globHIGHFired on the `engine::analyze` hot path by the CHEAP `blast_radius::cheap_check`: a `"$VAR/"`-shaped target whose variable resolves to empty in the snapshotted environment. The detector takes the environment as an injected map, so the empty-variable case is unit-tested without mutating the process environment.
blast_find_deleteMEDIUMFired on the `engine::analyze` hot path by the CHEAP `blast_radius::cheap_check` when a `find` invocation carries a `-delete` action. Run `tirith preview` to see how many files the predicate would actually remove before executing.
blast_rsync_deleteMEDIUMFired on the `engine::analyze` hot path by the CHEAP `blast_radius::cheap_check` when an `rsync` invocation carries `--delete` / `--delete-*` / `--del`. The destination operand is the side that loses files. Run `tirith preview` to understand the impact.
blast_large_file_countINFOSurfaced ONLY by `tirith preview -- "<cmd>"`, which walks the target tree (depth ≤ 5, capped at 100k files) and counts files. The hot path never walks the filesystem and never emits this rule — it is a simulator-only signal.

Repo Hooks5

Git/repo hooks that fetch, call the network, read credentials, or sudo

Rule IDSeverityDescription
repo_hook_network_callHIGHA repo hook body invokes curl, wget, nc, ncat, or netcat as a command word. Detected by the `tirith hooks` scanner over the hook body (treated as text, never executed).
repo_hook_credential_readHIGHA repo hook body references a well-known credential file or directory. Detected by the `tirith hooks` scanner over the hook body.
repo_hook_sudoHIGHA repo hook body uses `sudo` as a command word. Detected by the `tirith hooks` scanner over the hook body.
repo_hook_suspicious_shell_patternMEDIUMA repo hook body pipes into sh/bash/python/etc, decodes base64, or uses eval. Detected by the `tirith hooks` scanner over the hook body.
repo_hook_external_fetchMEDIUMA repo hook body references an external URL or runs a remote-package runner (npx / pnpm dlx). Detected by the `tirith hooks` scanner over the hook body.

Insecure Transport4

Plain HTTP, TLS downgrade, URL shorteners, and data URIs

Rule IDSeverityDescription
plain_http_to_sinkHIGHAn HTTP (not HTTPS) URL is passed to a download or execute command like curl, wget, or pip. Loopback addresses are exempt.
schemeless_to_sinkMEDIUMA URL without a scheme (no http:// or https://) is passed to a download or execute command. The tool may default to HTTP.
insecure_tls_flagsHIGHThe command includes flags that disable TLS certificate verification: -k, --insecure (curl), --no-check-certificate (wget), or similar.
shortened_urlMEDIUMThe URL uses a known URL shortener service (bit.ly, t.co, tinyurl.com, is.gd, v.gd, goo.gl, ow.ly). The real destination is hidden.

Shell Aliases4

Aliases and functions that shadow or override critical commands

Rule IDSeverityDescription
alias_overrides_critical_commandMEDIUM`tirith aliases scan` found an alias or function whose name matches a critical command. Surfaces the definition + location for review; the rule reports the shadowing, not maliciousness.
alias_contains_network_callHIGH`tirith aliases scan` parsed an alias/function body that invokes a network tool as a command word. The matched tool is reported in the finding detail.
alias_contains_credential_readHIGH`tirith aliases scan` parsed an alias/function body that references a known credential-file path. The matched path fragment is reported in the finding detail.
alias_recently_addedINFO`tirith aliases scan` flagged an alias/function whose defining rc file's mtime is within the last hour. Runtime-enumerated aliases (no source file) cannot be dated and never fire this rule.

Path Analysis3

Non-ASCII paths, homoglyphs, and encoded traversal sequences

Rule IDSeverityDescription
non_ascii_pathMEDIUMThe URL path contains bytes outside the ASCII range. These characters may visually resemble standard path components while pointing to a different resource.
homoglyph_in_pathMEDIUMA path segment mixes ASCII and non-ASCII characters and is within edit distance 2 of a sensitive keyword like install, setup, auth, token, login, or config.
double_encodingMEDIUMThe URL path contains double percent-encoding (%25 followed by hex), which encodes a percent sign itself. This is a common technique to bypass security filters.

Code File Scanning3

Obfuscated payloads, dynamic execution, and secret exfiltration in code files

Rule IDSeverityDescription
dynamic_code_executionHIGHA code file contains eval() or exec() near base64 decoding (atob, b64decode, Buffer.from), String.fromCharCode, or exec(compile()). This pattern hides the actual executed code.
obfuscated_payloadHIGHA code file contains long hex-encoded strings or heavily obfuscated string literals that may conceal malicious code.
suspicious_code_exfiltrationHIGHA code file contains network requests to non-standard hosts alongside access to sensitive variables or credential files.

Network Access3

Cloud metadata endpoints, private-network and SSRF-style access

Rule IDSeverityDescription
metadata_endpointCRITICALThe command accesses a cloud instance metadata endpoint: 169.254.169.254 (AWS/GCP/Azure) or 100.100.100.200 (Alibaba Cloud). These endpoints expose temporary credentials and instance configuration.
private_network_accessHIGHThe command targets a private or reserved IPv4 address (10.x, 172.16-31.x, 192.168.x, 127.x). In a remote-code-execution context, this may be SSRF or lateral movement.
command_network_denyCRITICALThe command targets a network destination that matches an entry in the policy network deny list. This is a policy-enforced block, not a heuristic detection.

Credential Detection3

API keys, tokens, private keys, and high-entropy secrets in input

Rule IDSeverityDescription
credential_in_textHIGHThe input matches a known credential pattern from a specific provider: AWS access keys, GitHub PATs, Stripe API keys, Slack tokens, and many others. The pattern is provider-specific with low false-positive rates.
high_entropy_secretMEDIUMA key=value assignment near a secret-related keyword contains a high-entropy string, detected via the p_random entropy scoring algorithm. This catches credentials without provider-specific patterns.
private_key_exposedCRITICALThe input contains a PEM private key block (-----BEGIN ... PRIVATE KEY-----). Private keys must never be shared, pasted, or transmitted in cleartext.

Clipboard & Provenance2

Hidden clipboard content and paste source/host mismatches

Rule IDSeverityDescription
clipboard_hiddenHIGHThe clipboard HTML contains content that is not visible in the plain-text paste: CSS-hidden elements, color-hidden text, hidden HTML attributes, or significant visible-HTML text missing from the plain paste.
paste_source_mismatchINFOFired by `engine::analyze` in paste context (`ScanContext::Paste`) ONLY. A companion browser extension (separate repo) writes a JSON record at `state-dir/clipboard_source.json` each time it sets the system clipboard, recording the source URL, page title, a SHA-256 of the copied content, and whether it saw hidden text. tirith reads (never writes) that record. The rule first confirms attribution: it computes `sha256(pasted_input)` and compares it to the record's `content_sha256` — if they differ, the paste did NOT come from the recorded source and NO finding is emitted (no false attribution). Only when the hashes match does it compare the destination host(s) of any URL in the pasted command against the recorded `source_url` host. If every destination host equals the source host there is no mismatch and no finding. A host mismatch alone is Info; a host mismatch plus any risk signal is High. The rule never echoes the pasted content or the source record into the finding beyond the hosts being compared.

Taint Tracking2

Executing or sourcing files that came from a risky or untrusted origin

Rule IDSeverityDescription
exec_of_tainted_fileHIGHFired by the exec hot path when the parsed command leader resolves to a path present in the taint store (`state-dir/taint.jsonl`). A path becomes tainted when `tirith fetch --save ./install.sh https://untrusted.example/install.sh` keeps a downloaded file at a known path. A later `bash ./install.sh` (or `./install.sh`) against that same path then fires this rule. The store is path-keyed: moving the file with `mv` loses the mark (documented limitation). The mark is NEVER auto-cleared by `chmod +x` or a `bash -n` parse check — only `tirith taint clear <file>` removes it.
command_sourced_from_tainted_fileMEDIUMFired by the exec hot path when the command leader is the `source` builtin or `.` and its first file argument resolves to a path in the taint store (`state-dir/taint.jsonl`). A path becomes tainted via `tirith fetch --save`. Like `exec_of_tainted_file`, the store is path-keyed (a `mv` loses the mark) and the mark is never auto-cleared — only `tirith taint clear <file>` removes it.

AI Config Drift2

Snapshot-diff changes to AI configs: hidden instructions and tool-use escalation

Rule IDSeverityDescription
ai_config_hidden_instruction_addedHIGHFired by `tirith ai diff` (`crate::rules::aifile::diff_findings`), which compares each current AI-config file to the last-known-safe snapshot at `state-dir/ai_config_snapshot.json` and reuses the shipping `agent_instruction_hidden` hidden-construct detection — HTML-comment directives and visually-hidden HTML elements (e.g. `style="display:none"`). It does NOT cover invisible-Unicode / zero-width text (zero-width, BIDI, and Unicode-tag invisibles in AI-config files are reported by the `config_invisible_unicode` rule, not this one). A newly-VISIBLE directive is flagged only when its normalized words (every ASCII-whitespace run collapsed to one space) are NOT already present as a contiguous run in the snapshot's whole-document word stream — so a Markdown reflow (re-wrapping a directive across a different number of lines) or blank-line churn does not fire, because the same words still appear contiguously in the old version. Hidden constructs (HTML comments / visually-hidden elements) are compared by a whitespace-normalized identity key, so a hidden block that was merely reformatted is likewise not treated as new. The trade-off of the word-stream containment is a rare false NEGATIVE: a genuinely-new directive whose words happen to already appear contiguously in the snapshot would be missed. Only content present in the NEW file but absent from the snapshot is reported; a removed line never fires this rule. The finding reports the added directive (truncated), never the whole file. This rule is never produced by the `engine::analyze` hot path or the `tirith scan` FileScan path — it is diff-triggered only, so it carries no tier-1 PATTERN_TABLE entry.
ai_config_tool_use_escalationHIGHFired by `tirith ai diff` (`crate::rules::aifile::diff_findings`) when a paragraph ADDED to an AI-config file since the snapshot carries a tool-use / capability directive — a run/exec/shell-spawn instruction, a network call, or a file-write directive — that was not in the snapshot. A paragraph counts as ADDED only when its normalized words (ASCII-whitespace runs collapsed to one space) are NOT already a contiguous run in the snapshot's whole-document word stream, so a reflow or blank-line regrouping of an EXISTING tool-use line does not fire — the same words still appear contiguously in the old version. The trade-off is a rare false NEGATIVE: a genuinely-new directive whose words already appear contiguously in the snapshot would be missed. The tool-use shape itself is matched per added paragraph; the finding reports the escalated directive (truncated), never the whole file. Like `ai_config_hidden_instruction_added`, it is diff-triggered only — never produced by `engine::analyze` or the FileScan path — so it carries no tier-1 PATTERN_TABLE entry.

Cloaking1

Servers returning different content to AI bots vs browsers

Rule IDSeverityDescription
server_cloakingHIGHThe URL serves measurably different content to different user agents (Chrome vs ClaudeBot, ChatGPT-User, Googlebot, curl). A word-level diff exceeding a threshold triggers this rule.

PDF1

Sub-pixel / hidden text in PDFs invisible to readers but parsed by LLMs

Rule IDSeverityDescription
pdf_hidden_textHIGHA PDF file contains hidden text layers that are invisible when rendered but can be extracted by text processing tools. These layers may contain hidden instructions for AI agents.

Command Cards1

Commands that do not match their signed command card

Rule IDSeverityDescription
command_card_mismatchHIGHFired by the exec hot path when the card supplied via `--card <path>` or `# tirith-card: <local-path>` verifies against a trusted, unexpired key BUT its `command` field does not equal (after trimming surrounding ASCII whitespace) the command being analyzed. The card is always read from disk; no remote fetch occurs on the hot path.

Repo Command Manifest1

Commands flagged against a repo allowed-command manifest

Rule IDSeverityDescription
repo_command_dangerous_patternHIGHFired by the exec hot path when a `.tirith/commands.yaml` is discovered for the current repo and the analyzed command matches a `dangerous[*].pattern` glob. The matched entry's `action` chooses the severity: `block` → High (Block), `warn` → Medium (Warn). A `dangerous` match takes precedence over the `allowed[]` catalogue — a command listed under BOTH still fires this finding, so a compromised manifest cannot allow-list its way out of a dangerous pattern (a `block` match still blocks). When no manifest file exists, this rule never fires.

Canary Tokens1

A planted canary (honeytoken) was touched

Rule IDSeverityDescription
canary_token_touchedHIGHFired by `engine::analyze` (paste + exec) and `engine::analyze_output` when a token present in the local canary store at `state-dir/canaries.jsonl` (`crate::canary`) appears in the scanned text. The match is a substring lookup, so a canary embedded in a larger blob (e.g. the output of `cat ~/.aws/credentials`) still fires. Generated tokens are CLEARLY SYNTHETIC — AWS-like uses the literal `AKIA00CANARY` infix (invalid for a real AWS key, since `0` is not in the base32 alphabet), GitHub-like uses `ghp_canary_`, etc. (see docs/canary-formats.md) — so they can never be mistaken for a real third-party credential and can never trigger an external provider's abuse / take-down workflow. By default detection is LOCAL-ONLY: it raises this finding and writes to the local audit log, with no phone-home. A canary MAY be created with an OPT-IN, user-self-hosted `--callback-url`, in which case detection ALSO fires one best-effort POST of `{kind, detected_at, context}` (never the token value) to that URL; a callback failure is logged and never blocks the verdict.