Checks where your agent is about to send bytes - domains, IPs, and GitHub repositories — against a reputation provider before the action runs, and blocks what comes back flagged. Covers shell commands, MCP calls, file reads, and WebFetch/WebSearch. Malanta by default, or bring your own vendor.
Operate and troubleshoot Malanta TrustGate — the Cursor security hooks that check domains/IPs (reputation) and behavioral attack patterns (ATR) before shell, MCP, file-read, and WebFetch/WebSearch actions. Use when a TrustGate hook blocks, warns, or asks for human approval on an action, when interpreting or explaining a verdict, when a domain or command was denied/allowed/paused for approval, or when configuring TrustGate's mode, scope, overrides, retention, or provider.
# Malanta TrustGate
TrustGate is a set of Cursor enterprise hooks. Before an agent action runs,
the matching hook extracts candidate domains/IPs, asks a reputation provider
(Malanta by default) whether to block, runs a behavioral (ATR) pass over the
content, and returns a verdict. Everything is local except the reputation
lookup (domains/IPs only) and an opt-in audit sink.
Hooks: `beforeShellExecution`, `beforeMCPExecution`, `beforeReadFile`,
`preToolUse` (WebFetch/WebSearch), and `beforeSubmitPrompt` (warn-mode-only).
## First rule: read the verdict, don't infer it
**Whether an action ran depends on the policy mode, not just reputation.**
An `allow` does NOT mean a host is clean — see the `trustgate-modes` rule
for per-mode behavior (warn mode allows on acknowledged retry, ask mode
pauses for a human approve/reject dialog, report-only never blocks, off
does nothing). Before stating anything about a host's
reputation, read the real verdict:
```bash
trustgate explain <decision_id> # decision_id is printed in deny messages
tail -n 20 ~/.cache/trustgate/decisions.log # JSON Lines, one verdict/line
```
Each log line carries `allow`, `label`, `reason`, `mode`, `hook`, and any
`warnings` (e.g. `warn-mode: acknowledged via retry`, `report-only mode:
would have denied`, or `ask mode: Cursor version ... degrading to a hard
deny` when `ask` ran on a Cursor build below the version floor). That is
the source of truth. **Exception: in `ask` mode do NOT investigate — see
the next section; hand the decision straight to the user.**
## If an action is paused for approval (ask mode) — STOP, ask the user
When a hook returns `permission:"ask"`, Cursor shows the user a native
**Approve / Reject dialog** and pauses the action *for the human to
decide*. (The dialog only appears for **shell** and **MCP** actions;
Cursor doesn't enforce `ask` for `WebFetch`/`WebSearch` or file reads, so
those are cleanly **denied** in ask mode, not paused — handle a deny
normally.) Your only job on an actual ask dialog is to get out of the way
and let the human decide:
- **Ask the user, plainly, whether to continue — then stop.** One or two
sentences, e.g.: *"TrustGate flagged `<host>` as `<LABEL>` and is asking
you to approve or reject this action. Approve in the dialog to continue,
or reject to cancel."* Then wait for the human.
- **Do NOT troubleshoot or investigate.** Don't run `trustgate explain` /
`doctor`, don't tail the decision log, don't analyze the score or rule.
The reason (host, label, score, `decision_id`) is already in the message
TrustGate returned — you have everything you need; looking things up is
noise.
- **Do NOT offer workarounds or a menu of options.** Don't suggest a
sandboxed/isolated fetch, `TRUSTGATE_ALLOW_USER_OVERRIDE`, disabling the
hook, rewording, retrying, or any other bypass. Presenting escape
hatches pressures the user past the exact safety control that just
fired. If they want an override, they'll ask.
- **Do NOT retry, reword, split, or re-route the action** to get around
the pause — that is trying to defeat a pending human decision.
- Only if the user *explicitly* asks "why was this flagged?" should you
explain the reason; only if they *explicitly* ask to proceed anyway
should you discuss an override (and only if an admin enabled it).
This is deliberately different from `enforce`/`warn`, where the outcome is
already decided and a `decision_id` explanation is useful. In `ask` the
human is mid-decision — help by being brief and stepping back, not by
working the problem.
## Diagnose a block
Use this when an action was **hard-denied** (enforce/warn) and the user
wants to understand or resolve it — NOT for an `ask` pause (see above,
just ask the user) and only investigate unprompted when a block is
actually blocking the user's work.
1. Get the `decision_id` from the deny message shown in the UI/agent.
2. `trustgate explain <decision_id>` — shows label, score, reason, hook.
3. Classify the deny by its `reason`:
- `... flagged <host> as <LABEL> (malicious score ...)` → **reputation**
deny. The host is on the block list / over threshold.
- `ATR rule <ID> (<category>) fired: ...` → **behavioral (ATR)** deny.
Independent of domain reputation; matched an attack shape.
- `... unconfigured ...` → no provider API key and
`TRUSTGATE_REQUIRE_CONFIGURED=true`. Run `trustgate setup`.
- `<provider> unavailable: ...` → provider error under fail-closed.
- `policy allowlist ...` / `out of scope ...` → allowed, not a block.
4. `trustgate doctor` — shows the mode, provider, scope, and config in
effect (start here when behavior is surprising).
## Overrides and the warn flow
- **warn mode**: a flagged host denies once, then the SAME action proceeds
on retry (a time-boxed grant is written). Do NOT auto-retry to bypass a
warn — that self-acknowledges before a human decides. Stop and surface it.
- **ask mode** (Cursor 3.11.25+): a flagged **shell or MCP** action is
emitted as `permission:"ask"` — Cursor shows a native approve/reject
dialog and pauses for the human. Handle it exactly as in **"If an action
is paused for approval"** above: ask the user plainly whether to
continue, then stop — do not investigate the verdict or offer bypasses.
`ask` degrades to a hard `deny` (never fails open) below the version
floor (`TRUSTGATE_ASK_MIN_CURSOR_VERSION`) AND on events Cursor doesn't
enforce `ask` for (`WebFetch`/`WebSearch`, file reads) — a deny there is
expected, not a bug.
- **Manual override** (only if an admin set `TRUSTGATE_ALLOW_USER_OVERRIDE=true`):
```bash
trustgate override --domain <host> --minutes 15 --reason "why" # per-host
trustgate override --repo <owner/repo> --minutes 15 --reason "why" # per-repository
trustgate override --owner <owner> --minutes 15 --reason "why" # per-account (broad)
trustgate override --clear [--domain <host> | --repo <owner/repo> | --owner <owner>]
```
Use the flag the deny message names. `--owner` allows every repository
under that account, so prefer `--repo` unless the deny was itself
owner-scoped.
Wildcards (`--domain '*'`) are rejected under domain scope; use
`TRUSTGATE_OVERRIDE_SCOPE=time` for a deliberate blanket window.
## Configuration
Env-file precedence (later wins), all read fresh per hook invocation:
`/etc/trustgate/env` (MDM) < `~/.config/trustgate/env` (per-user) <
workspace `.env` (**only if `TRUSTGATE_ALLOW_CWD_DOTENV=1`** — off by
default) < process env. A managed `/etc/trustgate/env` can pin keys via
`TRUSTGATE_LOCKED_KEYS` (those always win).
Common settings:
| Var | Effect |
| --- | --- |
| `TRUSTGATE_MODE` | `off` / `report-only` / `warn` (default) / `ask` / `enforce` |
| `TRUSTGATE_ASK_MIN_CURSOR_VERSION` | Min Cursor version for `ask` mode (default `3.11.25`; below it `ask` → hard deny) |
| `TRUSTGATE_REQUIRE_CONFIGURED` | Fail closed instead of inert-allow when no key |
| `TRUSTGATE_SCOPE_MODE` / `_PATHS` | Restrict which workspaces are inspected |
| `TRUSTGATE_ATR_DISABLE` | Skip the behavioral pass (keep reputation) |
| `TRUSTGATE_ATR_RULES_DIR` | Bring-your-own ATR rules |
| `TRUSTGATE_MIN_MALICIOUS_SCORE` | Block threshold (finite number) |
| `TRUSTGATE_BLOCK_LABELS` / `_ALLOW_LABELS` | Verdict labels that deny/allow |
| `TRUSTGATE_ALLOW_USER_OVERRIDE` | Enable `trustgate override` |
| `TRUSTGATE_RETENTION_DAYS` | Local audit retention window |
| `MALANTA_API_KEY` | Provider credential (env or env file only, never config.json) |
The API key must live only in process env or one of the env files above —
never in `config.json`, source, or a commit.
## Retention and data
Local stores under `~/.cache/trustgate/`: a reputation TTL cache and the
decision log + audit DB. Only indicators, verdicts, and ATR rule
*identities* are stored (ATR matches are a SHA-256 digest, never the raw
matched bytes); raw commands/files/prompts are never stored or transmitted.
```bash
trustgate purge --days 90 # or --all; deletes audit rows + log lines
trustgate export --out audit.jsonl
```
Retention is applied by `trustgate purge` (manual/cron), not on the hot
path. See `PRIVACY.md` for the full disclosure.
## Known false positives and workarounds
- **Docs mentioning credential paths** (e.g. `~/.ssh/id_rsa`, `~/.aws/...`)
can trip an ATR read-file rule. Escape hatch: `TRUSTGATE_ATR_DISABLE=true`
in an env file (leaves the reputation cascade active).
- **Batched CTI/threat-intel work** that puts a flagged domain literally in
a shell command line will (correctly) deny — the hook sees bytes, not
intent. Move domain-bearing calls into a helper that reads the domain
list from disk, so the flagged host never appears in argv.
- **Dotted config keys** (`git config user.email ...`) are scrubbed for
common tools; an untooled one may false-positive on its TLD-shaped
suffix.
For deployment/distribution details see the repository's `docs/` (admin,
architecture, providers).