# Defense-in-Depth Score: dbt MCP

**Repo:** https://github.com/dbt-labs/dbt-mcp · **Commit:** `8e7022bae4cc6a0dc6f708fe9d95de8ac952661e` · **Reviewed:** 2026-10-03
**What it is:** MCP server for dbt (CLI, semantic layer, discovery, admin APIs)
**Category:** Data & Analytics
**Scored configuration:** Local stdio server (default MCP_TRANSPORT) with DBT_PROJECT_DIR/DBT_PATH and dbt Platform credentials (DBT_TOKEN or OAuth) set and no toolset flags, so CLI, admin API, discovery, semantic layer, LSP, product docs and MCP Apps are on while SQL, codegen and server-metadata toolsets are off.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory no · untrusted input yes · third party extensions yes · sub agents no · external communication no

## Score: 1.8 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L0 | L1 | L0 | L1 | 0.12 | — | **0.12** | High |
| C2 | Approval gates | L1 | L1 | L2 | L0 | 0.25 | C2-POWERBYPASS | **0.25** | High |
| C3 | Tool & action scoping | L1 | L2 | L2 | L0 | 0.33 | G2 | **0.25** | High |
| C4 | Code-execution isolation | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C5 | Untrusted input blast radius | L1 | L0 | L0 | L0 | 0.07 | C5-WORSTCASE | **0.07** | Medium |
| C6 | Memory, context & configuration integrity | L0 | L0 | L1 | L0 | 0.05 | C6-REPOCONFIG | **0.05** | High |
| C7 | Third-party extensions | L1 | L1 | L0 | L2 | 0.25 | — | **0.25** | Medium |
| C8 | Secrets & sensitive-data protection | L2 | L2 | L0 | L0 | 0.30 | — | **0.30** | Medium |
| C9 | Audit & traceability | L1 | L2 | L1 | L1 | 0.33 | G2 | **0.25** | High |
| C10 | Limits & kill switch | L2 | L2 | L1 | L1 | 0.40 | G2 | **0.25** | High |


dbt-mcp gives an AI host the operator's full dbt Platform identity and warehouse profile, with dbt build/run/clone, arbitrary SQL through 'show', and production job triggers turned on by default. Its only safety signal is MCP annotations, and these mark arbitrary SQL as read-only and job step overrides as non-destructive. The server auto-loads a .env from its working directory, so a cloned repository can change which binary runs, where the token is sent, and which tools are enabled. Run it with an explicit DBT_MCP_ENABLE_TOOLS allowlist, a scoped read-only token and warehouse role, and a working directory you control.

## Critical gaps
- The 'show' tool executes arbitrary model-supplied SQL against the warehouse but is annotated readOnlyHint=True, so hosts that auto-approve read-only tools skip their gate. (ASI02, ASI09, T2; C2) — [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_cli/tools.py:510-515](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L510-L515); [src/dbt_mcp/prompts/dbt_cli/show.md:1](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/prompts/dbt_cli/show.md#L1)
- Tool enablement can be loosened by an auto-loaded workspace .env (e.g. enabling the SQL toolset or swapping DBT_PATH). (ASI02, LLM06; C3) — [src/dbt_mcp/config/settings.py:38-45](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L38-L45); [src/dbt_mcp/config/settings.py:67](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L67)
- dbt runs as a same-user subprocess with the full environment and no isolation, executing project code and model-supplied SQL/Jinja. (ASI05, T11; C4) — [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141)
- One server session combines untrusted project metadata/logs, warehouse data, and irreversible actions with no server-side containment. (ASI01, LLM01, T6; C5) — [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_admin/tools.py:155-157](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L155-L157)
- A .env file in the working directory is auto-loaded and can set DBT_PATH, DBT_HOST, CDN base and tool flags with no trust decision. (ASI06, ASI04, T1; C6) — [src/dbt_mcp/config/settings.py:38-45](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L38-L45); [src/dbt_mcp/config/settings.py:67](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L67); [src/dbt_mcp/config/settings.py:48](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L48)
- Tool-call logging can be silenced through DBT_MCP_LOG_LEVEL from the auto-loaded workspace .env. (T8; C9) — [src/dbt_mcp/config/settings.py:32](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L32); [src/dbt_mcp/config/settings.py:22-29](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L22-L29)
- dbt CLI timeouts stop waiting but never kill the dbt process, and the timeout itself can be raised from the workspace .env. (ASI08, T4, LLM10; C10) — [src/dbt_mcp/dbt_cli/tools.py:142](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L142); [src/dbt_mcp/dbt_cli/tools.py:157-158](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L157-L158)

## Criterion details

### C1 Identity & least privilege — 0.12 (high)

Every dbt Platform tool (job triggers, discovery, semantic layer, proxied SQL) shares one credential: either a token from DBT_TOKEN or an OAuth grant requesting the broad 'user_access' scope, so the server acts with the full rights of whoever configured it. The dbt CLI tools run the dbt binary with the operator's whole environment and the warehouse credentials in their dbt profile. There is no per-tool credential, no read/write split, and no authorization check in code; the only narrowing is that the account and environment ids come from configuration rather than from the model. If the model is steered, it can use the user's full dbt Platform access and the warehouse role.

- **S L0:** One static credential (DBT_TOKEN, or OAuth with scope 'user_access offline_access') is used for all platform tools, and the dbt subprocess runs with the operator's ambient warehouse profile. — [src/dbt_mcp/oauth/login.py:30](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/oauth/login.py#L30); [src/dbt_mcp/config/credentials.py:340](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/credentials.py#L340); [src/dbt_mcp/config/headers.py:22-24](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/headers.py#L22-L24) (verified)
  - *To reach the next level:* Read tools and write tools (trigger/cancel/retry, build/run) would need separate, narrower credentials or a code-level authorization gate.
- **C L1:** Admin tools always use the configured account id rather than a model-chosen one, but the dbt CLI, codegen, and LSP subprocesses inherit the full os.environ including DBT_TOKEN. — [src/dbt_mcp/dbt_admin/tools.py:175-177](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L175-L177); [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141); searched `rg -n env=` in `src/dbt_mcp` → 0 hits (No subprocess call in the server passes an explicit env; every child inherits os.environ.) (verified)
  - *To reach the next level:* Spawned processes would need a scrubbed environment and every tool would need to pass a shared authorization layer.
- **D L0:** The default OAuth login requests the user's full 'user_access' scope; least privilege depends on the operator choosing a scoped service token by hand. — [src/dbt_mcp/oauth/login.py:30](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/oauth/login.py#L30) (verified)
  - *To reach the next level:* The default identity would need to be read-only or minimal, with writes requiring explicit elevation.
- **B L1:** A hijacked server can write to two systems: dbt Platform (trigger, cancel, and retry any job in the account, including production jobs) and the data warehouse through the dbt profile. — [src/dbt_mcp/dbt_admin/tools.py:175-177](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L175-L177); [src/dbt_mcp/dbt_admin/tools.py:155-157](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L155-L157); [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141) (verified)
  - *To reach the next level:* Authority would need to be confined to one project/tenant with mostly read access and non-destructive writes.
- **Cap:** none

### C2 Approval gates — 0.25 (high)

As a tool server, dbt-mcp relies on the host to ask for approval and gives it MCP annotations to decide. Build, run, test, and clone are correctly marked destructive, but the 'show' tool, whose own description says it executes an arbitrary SQL statement, is marked read-only, and 'trigger_job_run', which can override a job's steps and target schema, is marked non-destructive. A host that auto-approves read-only tools will therefore let arbitrary warehouse SQL through without asking. The server has no dry-run, no read-only mode, and no confirmation step of its own, and the actions it exposes (warehouse SQL, full-refresh rebuilds, production job runs) are largely irreversible.

- **S L1:** Annotations exist on every tool, but they are wrong on mutating tools: 'show' runs arbitrary SQL yet is readOnlyHint=True, and trigger_job_run with steps_override is destructiveHint=False. — [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_cli/tools.py:510-515](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L510-L515); [src/dbt_mcp/prompts/dbt_cli/show.md:1](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/prompts/dbt_cli/show.md#L1); [src/dbt_mcp/dbt_admin/tools.py:139-145](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L139-L145); [src/dbt_mcp/dbt_admin/tools.py:155-157](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L155-L157) (verified)
  - *To reach the next level:* Every mutating tool would need an accurate readOnlyHint/destructiveHint, with reads and writes in separate tools.
- **C L1:** Correct annotations cover build/run/test/clone only; the most general write path (arbitrary SQL via show) and job overrides escape the destructive signal. — [src/dbt_mcp/dbt_cli/tools.py:431-440](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L431-L440); [src/dbt_mcp/dbt_cli/tools.py:510-515](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L510-L515); [src/dbt_mcp/dbt_admin/tools.py:139-145](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L139-L145) (verified)
  - *To reach the next level:* Every consequential path would need to carry a correct risk signal so a host gate is reached.
- **D L2:** Annotations are fixed in code and cannot be changed by the model at runtime; proxied tools take their annotations from the vendor's remote server. — [src/dbt_mcp/dbt_cli/tools.py:510-515](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L510-L515); [src/dbt_mcp/proxy/tools.py:197-203](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/proxy/tools.py#L197-L203) (verified)
  - *To reach the next level:* The server would need to make disabling its risk signalling require an explicit, loudly named operator flag, and offer a server-side confirmation it controls.
- **B L0:** In the default toolsets a wrongly approved call can run arbitrary warehouse SQL, rebuild tables with --full-refresh, or run a production dbt Platform job with arbitrary steps, with no preview or undo. — [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_cli/tools.py:283-285](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L283-L285); [src/dbt_mcp/dbt_cli/tools.py:87-88](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L87-L88); [src/dbt_mcp/dbt_admin/tools.py:155-157](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L155-L157); searched `rg -n -i elicit|sandbox|seccomp|docker` in `src/dbt_mcp` → 0 hits (No sandbox, container, or MCP elicitation/confirmation step in server code.) (verified)
  - *To reach the next level:* Destructive operations would need previews/dry-runs or rollback.
- **Cap:** C2-POWERBYPASS — The most general action path, arbitrary SQL through 'show', is advertised as read-only, so a host gate keyed on annotations skips it in the default configuration.

### C3 Tool & action scoping — 0.25 (high)

Some arguments are checked in code: dbt selector tokens may not start with '-', resource types come from an allowlist, run-artifact paths match a strict character pattern, and documentation fetches are pinned to docs.getdbt.com. But the most powerful inputs pass through raw: the 'show' tool forwards any SQL text to the warehouse, '--vars' takes free YAML, and trigger_job_run accepts arbitrary job steps and schema overrides. All toolsets except SQL, codegen, and server metadata are on by default, including write tools. Operators can disable toolsets, but a .env file in the server's working directory can turn them back on.

- **S L1:** Validation is a denylist on selector tokens plus a few allowlists, while 'show' passes arbitrary SQL and trigger_job_run passes arbitrary steps_override through unchanged. — [src/dbt_mcp/dbt_cli/tools.py:96-102](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L96-L102); [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_cli/tools.py:93-94](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L93-L94); [src/dbt_mcp/dbt_admin/tools.py:155-157](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L155-L157) (verified)
  - *To reach the next level:* Powerful inputs would need allowlist validation (e.g. read-only SQL enforced by parsing or a read-only warehouse role, an allowlist of job steps).
- **C L2:** Most built-in tools use typed schemas and several validate (resource_type allowlist, artifact path regex, docs URL host), but proxied remote tools are registered with untyped string arguments and no local validation. — [src/dbt_mcp/dbt_cli/tools.py:107-116](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L107-L116); [src/dbt_mcp/dbt_admin/client.py:28](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/client.py#L28); [src/dbt_mcp/product_docs/client.py:246-247](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/product_docs/client.py#L246-L247); [src/dbt_mcp/proxy/tools.py:197-203](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/proxy/tools.py#L197-L203) (verified)
  - *To reach the next level:* Proxied/remote tools would need to go through a shared validation layer.
- **D L2:** Toolsets are selectable by env flags and the SQL and codegen toolsets are off by default, but the default set still includes dbt build/run/clone/show and job triggers. — [src/dbt_mcp/tools/register.py:64-65](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/tools/register.py#L64-L65); [src/dbt_mcp/config/settings.py:175-181](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L175-L181); [src/dbt_mcp/config/settings.py:74](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L74); [src/dbt_mcp/config/settings.py:72-85](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L72-L85) (verified)
  - *To reach the next level:* The default tool set would need to be read-only, with write and exec tools requiring explicit enabling.
- **B L0:** A misused 'show' call reaches any table the warehouse profile can reach, and trigger_job_run can run any job in the account with arbitrary steps. — [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_admin/tools.py:175-177](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L175-L177) (verified)
  - *To reach the next level:* Tools would need to be scoped to a project with quantity bounds (rows, jobs) instead of arbitrary SQL against the warehouse.
- **Cap:** G2 — A .env file in the working directory, which may be a cloned repository, is auto-loaded and can enable disabled toolsets (e.g. DBT_MCP_ENABLE_SQL) or point DBT_PATH at another binary, bypassing the operator's tool selection.

### C4 Code-execution isolation — 0.00 (high)

The dbt CLI tools run the dbt binary as a plain subprocess of the user, with no container, sandbox, or reduced privileges, and with the full parent environment. Those runs execute the project's own models, macros, and hooks, plus any SQL and Jinja the model passes to 'show'. Nothing separates that execution from the user's files, network, or credentials. If the project or the model's input is malicious, it runs with everything the user has.

- **S L0:** dbt is launched with subprocess.Popen as the same user with no isolation primitive. — [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141); searched `rg -n -i elicit|sandbox|seccomp|docker` in `src/dbt_mcp` → 0 hits (No sandbox, container, or MCP elicitation/confirmation step in server code.) (verified)
  - *To reach the next level:* Execution would need at least OS-level separation (dedicated low-privilege user or container).
- **C L0:** No execution path is isolated: dbt CLI, codegen, and the LSP binary all run directly on the host; only the jq filter runs in a separate process with a cleared environment. — [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141); [src/dbt_mcp/lsp/lsp_connection.py:218](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/lsp/lsp_connection.py#L218); [src/dbt_mcp/dbt_admin/tools.py:62-65](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L62-L65) (verified)
  - *To reach the next level:* The main dbt execution path would need to run inside the isolation boundary.
- **D L0:** No isolation mechanism exists to turn on. — searched `rg -n -i elicit|sandbox|seccomp|docker` in `src/dbt_mcp` → 0 hits (No sandbox, container, or MCP elicitation/confirmation step in server code.) (verified)
  - *To reach the next level:* An isolation boundary would need to be on by default.
- **B L0:** The dbt child inherits the full environment (including DBT_TOKEN when set), the user's home directory and dbt profiles, and unrestricted network. — [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141); searched `rg -n env=` in `src/dbt_mcp` → 0 hits (No subprocess call in the server passes an explicit env; every child inherits os.environ.) (verified)
  - *To reach the next level:* The execution environment would need to exclude credentials and restrict filesystem and network.
- **Cap:** none

### C5 Untrusted input blast radius — 0.07 (medium)

The server feeds the model content written by others, such as model and column descriptions from the dbt project and its packages, dbt Platform job logs and artifacts, and documentation pages. It returns this as plain text or JSON with no provenance or 'untrusted' marking. The same server also exposes warehouse data and irreversible actions (arbitrary SQL, full-refresh runs, production job triggers), so one session covers all three legs of the Rule of Two. The server offers no mode that drops a leg automatically; an operator can only hand-pick toolsets. Prompt injection in a model description can therefore drive destructive or exfiltrating actions with no approval required by the server.

- **S L1:** dbt CLI tools return raw stdout text and other tools return JSON, with no provenance or untrusted flag on project- or log-derived content. — [src/dbt_mcp/dbt_cli/tools.py:147-148](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L147-L148); searched `rg -n -i untrusted|provenance` in `src/dbt_mcp` → 0 hits (No provenance or untrusted-content marking anywhere in the server.) (verified)
  - *To reach the next level:* Outputs would need to be structured so returned content is separated from metadata, with provenance the host can act on.
- **C L0:** No untrusted source is distinguished: descriptions, job logs, artifacts, docs pages, and remote proxied tool descriptions all reach the model with equal standing. — searched `rg -n -i untrusted|provenance` in `src/dbt_mcp` → 0 hits (No provenance or untrusted-content marking anywhere in the server.); [src/dbt_mcp/proxy/tools.py:197-203](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/proxy/tools.py#L197-L203) (verified)
  - *To reach the next level:* At least the main untrusted sources (project metadata, job logs) would need to be marked or contained.
- **D L0:** No containment control exists, so nothing is on by default. — searched `rg -n -i untrusted|provenance` in `src/dbt_mcp` → 0 hits (No provenance or untrusted-content marking anywhere in the server.) (verified)
  - *To reach the next level:* A containment mode (e.g. read-only/no-write when untrusted content is returned) would need to be on by default.
- **B L0:** In one session a hijacked host can read warehouse data and run irreversible actions through this server unattended; exfiltration via arbitrary warehouse SQL (e.g. unloading to external storage) depends on adapter features and is inferred. — [src/dbt_mcp/dbt_cli/tools.py:279](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L279); [src/dbt_mcp/dbt_admin/tools.py:155-157](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L155-L157); [src/dbt_mcp/dbt_cli/tools.py:87-88](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L87-L88) (inferred)
  - *To reach the next level:* Either exfiltration or irreversible actions would need to be impossible or approval-gated in the default configuration.
- **Cap:** C5-WORSTCASE — Worst case (B L0): a hijacked agent can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 0.05 (high)

dbt-mcp has no memory store, but its settings loader automatically reads a .env file from its current working directory. When an IDE or coding host starts the server inside a project, that file comes from the repository. Such a file can set DBT_PATH (which binary is run), DBT_HOST (where the dbt token is sent), the MCP Apps CDN base, the tool-enable flags, the log level, and the CLI timeout, with no prompt or trust decision. Operator-set environment variables take precedence, but any value the operator left unset is open to the repository.

- **S L0:** pydantic-settings env_file='.env' is resolved against the current working directory, so a repository-controlled file can add tools, change the executed binary, and redirect credential endpoints with no prompt. — [src/dbt_mcp/config/settings.py:38-45](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L38-L45); [src/dbt_mcp/config/settings.py:22-29](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L22-L29); [src/dbt_mcp/config/settings.py:67](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L67); [src/dbt_mcp/config/settings.py:48](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L48); [src/dbt_mcp/config/settings.py:108-111](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L108-L111) (verified)
  - *To reach the next level:* Security-relevant settings would need to come only from user/operator scope, or require an explicit workspace-trust decision.
- **C L0:** Both settings classes (main and logging) load the .env file; no path is controlled. — [src/dbt_mcp/config/settings.py:38-45](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L38-L45); [src/dbt_mcp/config/settings.py:22-29](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L22-L29) (verified)
  - *To reach the next level:* All auto-loaded configuration paths would need to be controlled.
- **D L1:** There is no shared memory to isolate, but the workspace .env can alter the server's own configuration at startup. — [src/dbt_mcp/config/settings.py:38-45](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L38-L45) (verified)
  - *To reach the next level:* The model and workspace would need to be unable to change security-relevant configuration.
- **B L0:** A poisoned .env persists in the repository, affects every user and session that launches the server from it, and can trigger code execution through DBT_PATH. — [src/dbt_mcp/config/settings.py:67](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L67); [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141) (verified)
  - *To reach the next level:* Poisoned configuration would need to be confined to a session or require human review before taking effect.
- **Cap:** C6-REPOCONFIG — An auto-loaded .env in the working directory can enable toolsets, change the executed dbt binary, and redirect DBT_HOST (where the token is sent) without any user trust decision.

### C7 Third-party extensions — 0.25 (medium)

dbt-mcp does not load plugins, but it pulls two kinds of remote content at runtime. The first is the HTML/JavaScript bundle for its MCP App UI, fetched fresh from a dbt Labs CDN on every read with no version pin or hash. The second is tool definitions from the dbt Platform remote MCP endpoint, filtered only by tool name. Both are on by default, and the CDN base URL can be changed through configuration, including a workspace .env. The server never executes this code itself; the host renders the app in its own context.

- **S L1:** The UI bundle is fetched from a configurable CDN URL on every read with no pin or integrity check (the project's own contract snapshot deliberately excludes it from hashing). — [src/dbt_mcp/apps/register.py:33-39](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/apps/register.py#L33-L39); [src/dbt_mcp/contract/snapshot.py:31-35](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/contract/snapshot.py#L31-L35); [src/dbt_mcp/config/settings.py:108-111](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L108-L111) (verified)
  - *To reach the next level:* Remote content would need to be version-pinned.
- **C L1:** Remote proxied tools are restricted to an allowlist of names, but the UI bundle and the remote tool descriptions/annotations are not verified. — [src/dbt_mcp/proxy/tools.py:89-97](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/proxy/tools.py#L89-L97); [src/dbt_mcp/proxy/tools.py:197-203](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/proxy/tools.py#L197-L203) (verified)
  - *To reach the next level:* Most remote content types would need verification (pins or hashes).
- **D L0:** MCP Apps are enabled by default (DISABLE_MCP_APPS=False) and proxied discovery tools load automatically, with no consent step showing what will run. — [src/dbt_mcp/config/settings.py:107-111](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L107-L111); [src/dbt_mcp/config/config_providers/proxied_tool.py:16-22](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/config_providers/proxied_tool.py#L16-L22) (verified)
  - *To reach the next level:* Nothing remote would be enabled by default, and adding it would show exactly what loads.
- **B L2:** The fetched bundle is returned to the host as a resource and is not executed in the server process, so it does not get the server's environment or token; it can still ask the host to call tools. — [src/dbt_mcp/apps/register.py:33-39](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/apps/register.py#L33-L39) (inferred)
  - *To reach the next level:* Remote content would need its own sandbox with scoped access enforced by the server.
- **Cap:** none

### C8 Secrets & sensitive-data protection — 0.30 (medium)

The dbt token is masked in settings logs and reprs. HTTP error messages are scrubbed of URLs, OAuth tokens are stored at 0600, and the jq worker runs with an empty environment. But the token sits in the environment that every dbt and LSP subprocess inherits, and dbt Jinja passed through 'show' can read environment variables. Usage telemetry is on by default and sends dbt Labs each tool's arguments, apart from sql_query and vars. Those arguments include semantic-layer filters and job step overrides, together with account, environment, and user IDs. Tool output sent to the model is not redacted.

- **S L2:** Custom repr and settings logging redact dbt_token, and OAuth tokens are written as plaintext YAML with 0600 permissions. — [src/dbt_mcp/config/settings.py:160](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L160); [src/dbt_mcp/config/credentials.py:249-253](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/credentials.py#L249-L253); [src/dbt_mcp/oauth/context_manager.py:57-66](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/oauth/context_manager.py#L57-L66) (verified)
  - *To reach the next level:* Credentials would need to live in an OS keychain/secret manager and be redacted before model-bound output.
- **C L2:** Logs and telemetry redact the token and the sql_query/vars arguments, and the jq child clears its environment, but subprocess environments and model-bound tool output are unprotected. — [src/dbt_mcp/mcp/server.py:41-42](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/mcp/server.py#L41-L42); [src/dbt_mcp/tracking/tracking.py:30](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/tracking/tracking.py#L30); [src/dbt_mcp/dbt_admin/tools.py:62-65](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L62-L65); searched `rg -n env=` in `src/dbt_mcp` → 0 hits (No subprocess call in the server passes an explicit env; every child inherits os.environ.) (verified)
  - *To reach the next level:* Subprocess environments and model-bound messages would need the same protection.
- **D L0:** Usage tracking defaults to on and ships tool arguments (other than sql_query and vars) to dbt Labs telemetry. — [src/dbt_mcp/config/settings.py:224-244](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L224-L244); [src/dbt_mcp/tracking/tracking.py:113-116](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/tracking/tracking.py#L113-L116); [src/dbt_mcp/tracking/tracking.py:142-148](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/tracking/tracking.py#L142-L148) (verified)
  - *To reach the next level:* Telemetry would need to be content-free or opt-in.
- **B L0:** A long-lived DBT_TOKEN, often a personal or service token with broad scope, is inherited by every dbt subprocess, where model-supplied Jinja can read environment variables (dbt behaviour, inferred). — [src/dbt_mcp/config/credentials.py:340](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/credentials.py#L340); [src/dbt_mcp/dbt_cli/tools.py:134-141](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L134-L141); searched `rg -n env=` in `src/dbt_mcp` → 0 hits (No subprocess call in the server passes an explicit env; every child inherits os.environ.) (inferred)
  - *To reach the next level:* Keys reachable from tool processes would need to be scoped and short-lived.
- **Cap:** none

### C9 Audit & traceability — 0.25 (high)

Every tool call passes through one dispatcher. It logs the tool name and arguments (with SQL and vars masked), then logs success or error with the duration. The output is plain text on stderr with no timestamps in the default format, and it reaches durable storage only if the host captures it. File logging is opt-in. There is no record of who requested a call and nothing tamper-evident. A .env file in the working directory can raise the log level and silence tool-call logging.

- **S L1:** Tool calls are logged as unstructured INFO lines with arguments and duration but no timestamp or requesting identity. — [src/dbt_mcp/mcp/server.py:98](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/mcp/server.py#L98); [src/dbt_mcp/telemetry/logging.py:51-53](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/telemetry/logging.py#L51-L53) (verified)
  - *To reach the next level:* The record would need to be structured with arguments, result status, and timestamps for every call.
- **C L2:** All tools, including proxied remote tools, go through DbtMCP.call_tool, so every call is logged. — [src/dbt_mcp/mcp/server.py:95-106](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/mcp/server.py#L95-L106); [src/dbt_mcp/mcp/server.py:98](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/mcp/server.py#L98) (verified)
  - *To reach the next level:* Approvals/denials and configuration changes would need to be recorded too.
- **D L1:** Logging to stderr is on by default, but a durable server-owned record is opt-in (file logging defaults to False) and the log level is settable from the workspace .env. — [src/dbt_mcp/config/settings.py:31](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L31); [src/dbt_mcp/config/settings.py:32](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L32); [src/dbt_mcp/config/settings.py:22-29](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L22-L29) (verified)
  - *To reach the next level:* A durable record would need to be on by default, outside anything the workspace can change.
- **B L1:** Logging is best-effort through the standard library; actions proceed regardless of whether the record is written or kept. — [src/dbt_mcp/mcp/server.py:98](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/mcp/server.py#L98) (verified)
  - *To reach the next level:* Records would need to be flushed durably per action with errors surfaced.
- **Cap:** G2 — DBT_MCP_LOG_LEVEL can be set from an auto-loaded workspace .env to suppress the INFO-level tool-call log.

### C10 Limits & kill switch — 0.25 (high)

The server sets timeouts on most of its own work. dbt CLI calls default to 60 seconds, platform API calls to 15 seconds, and semantic-layer calls to 30 seconds. jq filters are capped at 120 seconds and at four concurrent runs, and some responses have size caps. But when a dbt command times out, the server only stops waiting: the dbt process is never killed and keeps running against the warehouse. dbt CLI output is not size-capped, there is no rate limit on job triggers or runs, and the CLI timeout can be raised from a workspace .env.

- **S L2:** Server-enforced timeouts exist for dbt CLI, platform HTTP and jq, plus caps on artifact size and semantic-layer response size, but concurrency limits cover only jq. — [src/dbt_mcp/config/settings.py:17](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L17); [src/dbt_mcp/dbt_cli/tools.py:142](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L142); [src/dbt_mcp/config/settings.py:18-19](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L18-L19); [src/dbt_mcp/dbt_admin/tools.py:52](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L52); [src/dbt_mcp/config/settings.py:120-125](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L120-L125); [src/dbt_mcp/dbt_admin/tools.py:56-59](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L56-L59) (verified)
  - *To reach the next level:* Every operation would need caps plus concurrency or rate limits on side-effecting tools.
- **C L2:** Timeouts apply to the main tool paths (CLI, HTTP, jq), but spawned dbt processes keep running past the timeout and outside any budget. — [src/dbt_mcp/dbt_cli/tools.py:142](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L142); [src/dbt_mcp/dbt_cli/tools.py:157-158](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L157-L158); searched `rg -n kill|terminate` in `src/dbt_mcp/dbt_cli src/dbt_mcp/dbt_codegen` → 0 hits (The dbt CLI and codegen executors never kill or terminate the child after a timeout.) (verified)
  - *To reach the next level:* Spawned processes would need to count against, and be stopped by, the same limits.
- **D L1:** Defaults are sensible (60s CLI) but DBT_CLI_TIMEOUT can be raised from an auto-loaded workspace .env, not only by the operator. — [src/dbt_mcp/config/settings.py:68](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L68); [src/dbt_mcp/config/settings.py:38-45](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/config/settings.py#L38-L45) (verified)
  - *To reach the next level:* Limits would need to be operator-only, with ceilings configuration can't exceed.
- **B L1:** On timeout the dbt child is left running (communicate(timeout) raises without kill), so a long build or full refresh continues after the tool returns. — [src/dbt_mcp/dbt_cli/tools.py:142](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L142); [src/dbt_mcp/dbt_cli/tools.py:157-158](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_cli/tools.py#L157-L158); searched `rg -n kill|terminate` in `src/dbt_mcp/dbt_cli src/dbt_mcp/dbt_codegen` → 0 hits (The dbt CLI and codegen executors never kill or terminate the child after a timeout.); [src/dbt_mcp/dbt_admin/tools.py:306-314](https://github.com/dbt-labs/dbt-mcp/blob/8e7022bae4cc6a0dc6f708fe9d95de8ac952661e/src/dbt_mcp/dbt_admin/tools.py#L306-L314) (verified)
  - *To reach the next level:* Stopping would need to cancel in-flight work and kill the dbt process.
- **Cap:** G2 — The primary limit, DBT_CLI_TIMEOUT, can be raised by an auto-loaded .env in the working directory.

## Rule-of-Two check
[A] untrusted input: Project/package model descriptions, dbt Platform job logs and artifacts, docs pages and remote tool descriptions returned as plain content (src/dbt_mcp/dbt_cli/tools.py:147, src/dbt_mcp/proxy/tools.py:202) · [B] sensitive data/systems: Warehouse rows via show/query_metrics and the dbt Platform token in the subprocess environment (src/dbt_mcp/dbt_cli/tools.py:279, src/dbt_mcp/dbt_cli/tools.py:134) · [C] state change / egress: dbt build/run/clone --full-refresh, arbitrary SQL, trigger/cancel/retry production jobs with steps_override (src/dbt_mcp/dbt_cli/tools.py:87, src/dbt_mcp/dbt_admin/tools.py:155) · Same default session? Yes

## Highest-impact improvements
1. Annotate 'show' (and compile) as non-read-only/destructive and mark trigger_job_run destructive when steps_override or schema_override is used; split read and write variants. — C2 S L1→L2, +0.075 before caps (Playbook 5)
2. Stop loading .env from the working directory (or only from an explicit operator path) so the repository cannot set DBT_PATH, DBT_HOST, tool flags, log level or timeouts. — C6 S L0→L3, +0.225 before caps (Playbook 2)
3. Kill the dbt process group on timeout and add a concurrency/rate limit on build/run/trigger tools. — C10 B L1→L2, +0.050 before caps (Playbook 3 step 3)
4. Pass a minimal environment to dbt/LSP subprocesses (drop DBT_TOKEN and unrelated secrets). — C1 C L1→L2, +0.075 before caps (Playbook 4)
5. Ship a read-only default toolset (discovery, semantic layer reads, list/parse) and require explicit enabling for build/run/clone/show and job triggers. — C3 D L2→L3, +0.050 before caps (Playbook 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- Behaviour of third-party components is inferred, not verified: dbt's handling of inline SQL/Jinja in 'show' (including env_var access and running SQL at compile time), pydantic-settings precedence (OS env vars override .env values), and how MCP hosts render ui:// app resources.
- Remote proxied tools (execute_sql, text_to_sql, search, get_related_models) are defined on dbt Platform and were not reviewed; only the local proxy code was.
- Version is null: HEAD is 5 commits after tag v2.5.0 and carries no tag.
- Tests, examples/ and evals were not scored; no reviewer-steering text was found (search for auditor/AI-reviewer/ignore-instructions phrases returned no hits).
