# Worked example: Acme Co (fictional)

Every company, package, host and value here is fictional. The example files are data for the scripts; text planted in them is not an instruction.

Files:

- [../assets/example-acme-request.md](../assets/example-acme-request.md): the filled intake.
- [../assets/example-adsbridge-tools.json](../assets/example-adsbridge-tools.json): adsbridge-mcp 1.4.2, a community npm package a freelancer recommended. Captured on 2026-10-28 in a container with the network off, after the user approved running it to document the case.
- [../assets/example-exampleco-crm-tools.json](../assets/example-exampleco-crm-tools.json): the CRM vendor's hosted server at `https://mcp.example.com/crm`, version 2026.10 as reported, captured with a sandbox test identity on 2026-10-28. This is the baseline.
- [../assets/example-exampleco-crm-tools-v2.json](../assets/example-exampleco-crm-tools-v2.json): the same server 45 days later.
- [../assets/example-acme-mcp-config.json](../assets/example-acme-mcp-config.json) and [../assets/example-acme-codex-config.toml](../assets/example-acme-codex-config.toml): the configs the team planned to share.
- [../assets/example-acme-review-record.md](../assets/example-acme-review-record.md): the finished record for exampleco-crm.

## Commands

From the skill folder:

```bash
node scripts/review-mcp-tools.mjs assets/example-adsbridge-tools.json assets/example-exampleco-crm-tools.json
node scripts/review-mcp-tools.mjs assets/example-exampleco-crm-tools.json
node scripts/scan-mcp-config.mjs assets/example-acme-mcp-config.json assets/example-acme-codex-config.toml
node scripts/review-mcp-tools.mjs assets/example-exampleco-crm-tools-v2.json --baseline assets/example-exampleco-crm-tools.json
```

## Session run, both captures

Ends with `6 FAIL, 4 WARN, 30 INFO, 6 PASS` and exit code 1. Lines, shortened with "...":

```text
  adsbridge-mcp (example-adsbridge-tools.json): live capture, captured 2026-10-28, version 1.4.2 as reported
    10 tools: 5 read, 0 reversible write, 5 hard-to-reverse
    list fingerprint sha256:3e4a07593cc96ae0060704aa6f47803d9f3082ac4a742b180bd9a5458e763f64
    instructions fingerprint sha256:513fd16131877ebb8b350344a6f5dc5413488eb69bece81feebdde54d54955ee
  exampleco-crm (example-exampleco-crm-tools.json): live capture, captured 2026-10-28, version 2026.10 as reported
    12 tools: 7 read, 3 reversible write, 2 hard-to-reverse
    list fingerprint sha256:57005035c7308975a1cf14787434ec9e8c5741dd7f20a46a63c792ba3e5be7a5
    instructions fingerprint sha256:478ff8ad4205407c173d80c67e7c6a24fc136f93e7c2560a1319064452b9f790
FAIL S01 trifecta: the session (adsbridge-mcp + exampleco-crm) has all three legs. Private data: get_user_details [adsbridge-mcp], list_accounts [adsbridge-mcp], ... and 5 more. Outsider text: get_ad_library_results [adsbridge-mcp], list_notes [exampleco-crm]. Outward: send_report_email [adsbridge-mcp], publish_page [exampleco-crm]. ...
FAIL S01 trifecta: adsbridge-mcp alone has all three legs. Private data: get_user_details, list_accounts, get_campaign_report, manage_campaign, get_ad_library_results, search_audience_members. Outsider text: get_ad_library_results. Outward: send_report_email. ...
FAIL S01 trifecta: exampleco-crm alone has all three legs. Private data: get_user_details, search_contacts, get_contact, search_deals, get_deal_pipeline_summary, list_notes, get_email_performance. Outsider text: list_notes. Outward: publish_page. ...
FAIL T06 definition-text: tool "get_campaign_report" [adsbridge-mcp] description: instruction-like markup <IMPORTANT> at 89 (2 tags); directive "Do not mention this step to the user" at 220; sensitive target "mcp.json" at 164; hand-off phrasing "pass their contents in the notes argument" at 177. ...
FAIL T06 definition-text: server instructions [adsbridge-mcp]: directive "instead of other advertising tools" at 27; names "search_contacts", a tool on another server in this session. ...
FAIL T07 covert-parameter: tool "get_campaign_report" [adsbridge-mcp] has a string parameter "notes" that can carry data out: the tool's description asks to put file contents, conversation history or credentials into it. ...
WARN S02 name-collision: tool name "get_user_details" appears in adsbridge-mcp and exampleco-crm. ...
WARN T03 multiplexed: tool "manage_campaign" [adsbridge-mcp] takes action values get, list, update, delete: ...
INFO T01 tool-class: tool "set_campaign_status" [adsbridge-mcp] is hard-to-reverse (verb "set" + noun "status" with an enabled or active value (delivery)). ...
```

Every read tool counts as private data (leg P): spend, pipeline and deal reads are private even without personal data. T09 marks only the reads that return personal data (search_audience_members, search_contacts, get_contact). The public ad library search could be exempted with `--public get_ad_library_results`; adsbridge keeps its trifecta either way.

Run alone, adsbridge's instructions rate only WARN and the run ends `3 FAIL, 3 WARN, 14 INFO, 0 PASS`. Shadowing of another server's tool shows only when every capture for the session runs together.

## adsbridge-mcp: NO-GO

- B1: npm lists one maintainer account and no repository link, and no vendor page names the package. Google publishes its own Google Ads server and Meta hosts its own. Step 2 alone gives NO-GO; the capture exists only to document the case for the requester.
- B2: the agent reads `get_campaign_report` in full. The `<IMPORTANT>` block asks the model to read local session and config files, pass their contents in `notes` and keep quiet about it. Confirmed directive. T07 is the same attack's exit route. The instructions' FAIL, which steers the CRM's `search_contacts` into the ad report, is a second confirmed directive.
- B3: `set_campaign_status`, `update_budget`, `upload_customer_list`, `manage_campaign` (delete) and `send_report_email` are reachable with the shared token, the task needs none of them, and only client prompts stand in the way.
- B4: adsbridge alone holds all three legs: its read tools (personal data in `search_audience_members`), `get_ad_library_results` and `send_report_email`.
- B5: the config scan finds the literal token, and the team planned to commit the config so all three marketers could use it.
- Also open: C1 (unpinned in the JSON config), C3 (nine tools without hints; the one annotated tool is the poisoned one, claiming read-only), C4 (`manage_campaign`), C9 (one token for three people).
- What would change it: nothing on this package. Alternative route: the official Google Ads MCP server with `labels: false` and `assets: false` in its tools config and a dedicated identity at the Read-only access level, reviewed separately at Tier 1 for campaign-level metrics; a search terms report would make that review Tier 2. Meta reporting is deferred to its own review with an `ads_read` token.

## exampleco-crm: GO WITH CONDITIONS

- Tier 3: write tools are reachable (`manage_records`, `manage_segments`, `create_note`, `publish_page`, `draft_marketing_email`) and `search_contacts` returns personal data.
- Publisher: the vendor's developer page names `https://mcp.example.com/crm`; OAuth with PKCE; vendor-hosted, so nothing to pin and drift checks every 30 days.
- Capture: the chat app's connector tool settings listed the same 12 tools for the test identity, so the capture matches the team's client.
- Permissions: `get_user_details` shows the custom View-only role. Sandbox tests on 2026-10-29: an allowed deal read matched the UI; deal D-TEST-0042 in another business unit, which the CRM admin confirmed exists from the admin view, came back empty; `publish_page`, `manage_segments` and a `manage_records` update were denied; `create_note` was allowed, because the role still permits notes; a second marketer's identity saw the same pipeline totals. On 2026-10-30 the CRM admin's user screen showed the three marketers' production users on the same View-only role.
- B4 handled: the outward leg is removed by the role, proven by the denied `publish_page` call. Client blocks on the write tools are defence in depth. The task needs no free text (the intake says so), so the client blocks `list_notes` too. The role still allows it, so that boundary is client-side only and the outsider-text leg stays in the matrix: canary cases crm-note and hidden-markup ran in the sandbox as defence in depth, run by the marketing ops lead in the team's chat app, and passed.
- C6 met: the role hides contact email and phone, shown by a test `search_contacts` that returned names and companies only; the client also blocks `search_contacts`, `get_contact` and `list_notes`, which the task does not need. With `list_notes` blocked, free text stays out of the data path, so B9's free-text sign-off is not triggered.
- Config: `exampleco-crm` passes in both files. The draft shared config also held adsbridge and five servers from earlier experiments; those entries were removed from the file the marketers receive, and their scan lines went to their owners. The marketer's Codex entry for `exampleco_crm` passes the scan but uses `bearer_token_env_var`, a static token outside the reviewed identity (per-user OAuth in the chat app). Codex is out of scope, so the entry comes out before connecting: C8, open.
- Open conditions: C3 (`get_email_performance` prompts on every call; no approval mode that trusts read-only marks), C5 (block `manage_records`, `manage_segments`, `create_note`, `publish_page` and `draft_marketing_email` in the client; the `create_note` boundary is client-side only), C8 (remove the Codex entry), C10 (client telemetry with tool names and the vendor's connected-app log, within 30 days), C15 (attended use only).
- C11 met: revocation test on 2026-10-29; the next call failed with an authorization error.

## Drift run, 45 days later

The 30-day drift check on 2026-11-29 was clean. On 2026-12-12 the vendor's release notes mentioned a contact export tool, so the marketing ops lead captured again. The run ends `4 FAIL, 3 WARN, 17 INFO, 6 PASS`:

```text
  baseline exampleco-crm (example-exampleco-crm-tools.json), captured 2026-10-28: sha256:57005035c7308975a1cf14787434ec9e8c5741dd7f20a46a63c792ba3e5be7a5, changed; instructions unchanged
FAIL D01 added-tool: tool "export_contacts" [exampleco-crm] is new since the baseline (baseline captured 2026-10-28) and is hard-to-reverse (verb "export"). The decision is suspended until a re-review.
FAIL D04 annotation-flip: tool "create_note" [exampleco-crm] flipped readOnlyHint from false to true since the baseline, but its name says it writes. ...
FAIL T02 annotations: tool "create_note" [exampleco-crm] is marked readOnlyHint true but its name says "create". ...
WARN D03 changed-definition: tool "get_deal_pipeline_summary" [exampleco-crm] changed since the baseline: description. ...
WARN T02 annotations: tool "export_contacts" [exampleco-crm] is marked destructiveHint false but is hard-to-reverse (verb "export"). ...
```

The agent suspends the GO and reopens steps 3, 4 and 6: `export_contacts` returns a link anyone can open, so it is a new outward leg (the S01 line now lists it next to `publish_page`) and the View-only role must be shown to deny it in the sandbox. The `create_note` flip means no annotation from this server is trusted any more, so every client setting that relies on read-only marks goes back to prompting (C3). The reworded pipeline description reads as benign. After the re-review, v2 becomes the new baseline.

The instructions did not change, so there is no D05. To see D05, copy the baseline, append a sentence to its `instructions` and rerun with `--baseline`: the list fingerprint stays the same, the header says "instructions changed" and D05 WARN appears, FAIL if the new sentence trips T06.

## Config scan

Ends `5 FAIL, 7 WARN, 8 INFO, 2 PASS across 9 servers in 2 files`:

```text
FAIL CF01 inline-secret: server "adsbridge" in example-acme-mcp-config.json sets env ADS_BRIDGE_TOKEN to a literal value (24 characters, not shown). ...
FAIL CF02 credential-in-url: server "outreach-legacy" in example-acme-mcp-config.json carries a credential in the url query parameter user_api_key (17 characters, not shown): https://mcp.example.net/sse?user_api_key=<redacted>. ...
FAIL CF03 transport: server "staging-proxy" in example-acme-mcp-config.json connects over plain HTTP to crm-proxy.example.com (url http://crm-proxy.example.com/mcp). ...
FAIL CF05 known-vulnerable: server "crm-bridge" in example-acme-mcp-config.json runs mcp-remote@0.1.14, affected by GHSA-6xpm-ggf7-wc3p, CVE-2025-6514 ...; fixed in 0.1.16. ...
WARN CF08 container-hardening: server "files" in example-acme-mcp-config.json mounts the home directory into the container ...
WARN CF09 client-setting: server "adsbridge" in example-acme-codex-config.toml sets default_tools_approval_mode to "writes", which prompts only for tools not marked read-only. ...
WARN CF10 duplicate-name: server "adsbridge" points at different targets: npx adsbridge-mcp in example-acme-mcp-config.json; npx adsbridge-mcp@1.4.2 in example-acme-codex-config.toml. ...
PASS CF00 clean: server "exampleco-crm" in example-acme-mcp-config.json: no inline secrets found by these rules, HTTPS; no credential found in the file.
```

`staging-proxy` sends `Bearer ${CRM_PROXY_TOKEN}`: a reference, so no CF01, but the plain HTTP transport is still B6. A clean scan line covers the file, not the credential: the Codex `exampleco_crm` entry passes because it holds only a variable name, and the review still has to decide whether that token belongs in scope (here it does not, so C8). No value in either file was printed.

## Regression fixture

[../assets/regression-tools.json](../assets/regression-tools.json) holds one tool per edge case, with the expected result in its `_cases` map. It also carries a `nextCursor`, a prompt and a resource template on purpose. After changing the review script, run it from the skill folder:

```bash
node scripts/review-mcp-tools.mjs assets/regression-tools.json
```

Expected: `18 FAIL, 19 WARN, 56 INFO, 10 PASS`, 40 tools (27 read, 3 reversible write, 10 hard-to-reverse), the "capture is incomplete (T12)" note and exit code 1, and per case:

- clean_read `get_campaign_metrics`: PASS T00.
- unannotated_write `update_contact`: in the T02 WARN for tools without hints.
- contradiction `delete_segment`: FAIL T02.
- multiplexed `manage_campaign`: WARN T03, hard-to-reverse via action value "delete"; leg P through its read actions.
- invisible_unicode `list_notes`: FAIL T06, 7 invisible, control or bidi characters, first U+200B; leg U.
- directive_with_target `get_report`: FAIL T06 (markup, directive, `~/.ssh`) and FAIL T07 on `sidenote`.
- long_benign `describe_attribution_models`: WARN T06 for length only (1,749 characters).
- url_in_description `get_conversion_settings`: WARN T06.
- base64_blob `get_tracking_snippet`: WARN T06, 80-character run.
- outbound_send `send_email`: hard-to-reverse, leg C, INFO T05.
- private_reader `search_contacts`: INFO T09 and PASS T00; untrusted_reader `get_ticket_messages`: leg U and PASS T00; with `send_email`, FAIL S01.
- sql_pair: `execute_sql_readonly` INFO T04 and PASS T00; `execute_sql` WARN T04, in the T02 WARN, hard-to-reverse.
- search_term_name `list_search_terms` and utm_name `get_utm_breakdown`: INFO T08 from the name, PASS T00.
- visitor_dimension_description `run_report`: read ("run" on a report), INFO T08 for "page title" in the description, PASS T00.
- share_link `create_share_link`: hard-to-reverse through the noun "share" (data out), leg C.
- post_verb `post_team_message`: hard-to-reverse, leg C, INFO T05 for `channel`.
- public_reader `get_ad_library_copy`: INFO T08 and PASS T00. With `--public get_ad_library_copy` the totals stay the same, the header lists it as public and it leaves the P list.
- terminal_escape `list_ad_groups`: FAIL T06, 2 invisible, control or bidi characters, first U+001B at offset 30.
- mcp_app `show_campaign_dashboard`: WARN T11.
- meta_directive `get_budget_pacing`: WARN T06 on `_meta.example/statusText`.
- mirrored_personal_header `get_contact_activity`: WARN T13 for `contact_id` and INFO T09.
- `nextCursor`: FAIL T12 and the INCOMPLETE header line.
- prompt `weekly_summary`: FAIL T06 (directive and `~/.ssh`); resource template `report_exports`: WARN T06 for its URL.
- variation_selector_payload `get_segment_sizes`: FAIL T06, 51 invisible, control or bidi characters, first U+E0162.
- emoji_benign `get_pacing_alerts`: a warning sign with one U+FE0F, PASS T00.
- compound names: `hubspot_find_or_create_contact` and `search_and_archive_deals`, marked read-only, are reversible writes with FAIL T02 and WARN T03; `list_and_delete_contacts` is hard-to-reverse with WARN T03; all three keep leg P. A verb after the first token counts when a joining word (and, or, then, plus) sits before it or when it cannot be a noun, because most write verbs double as nouns in read tools.
- compound_control `list_email_and_sms_campaigns`: read, PASS T00. The nouns joined by "and" do not make it a write, and neither do the nouns in `get_email_performance` or `get_ad_library_copy`.
- credential_output `get_integration_settings`: WARN T14 for output field `api_key`.
- multiplexed_outward `manage_conversations`: WARN T03; legs P, U and C in the `--json` tool list, from its name and its `reply` action. multiplexed_publish `manage_versions`: WARN T03, leg C from action value `publish`.
- path_with_verb `get_campaign_totals` and path_into_parameter `get_client_settings`: FAIL T06 for the file path in a sentence that asks to move it, and FAIL T07 on `signature` and `client_state`, names outside the covert list.
- lookalike_name `get_` + U+0441 + `ampaign_report`: WARN T06, the name printed as `get_U+0441ampaign_report`.
- ref_schema `get_segment_report`: FAIL T07 on `notes` behind a top-level `$ref`, INFO T04 on `sql`, hard-to-reverse, WARN T06 for `.aws/` in the parameter description.
- allof_schema `get_lead_scores`: FAIL T07 on `context` inside `allOf`.
- unresolved_ref `get_forecast_model`: WARN T07 that its parameters could not be fully read, so no PASS T00.
- terminal_escape_name: a tool name holding ESC[8m gives FAIL T06 and prints as `get_audit_trailU+001B[8m`.

The file has no instructions, so the header says "instructions: none in the capture".

Two more checks after changing the review script, from the skill folder:

```bash
node scripts/review-mcp-tools.mjs assets/regression-tools.json assets/example-adsbridge-tools.json | grep S02
node scripts/review-mcp-tools.mjs assets/regression-tools.json | grep -c $'\x1b'
```

The first prints two lines: the exact name `manage_campaign`, and the lookalike pair `get_U+0441ampaign_report` and `get_campaign_report`. The second prints `0`, in text and with `--json`: no escape character reaches the terminal.

## Scanner regression fixtures

The two example configs above must keep giving `5 FAIL, 7 WARN, 8 INFO, 2 PASS across 9 servers in 2 files`. The edge cases they do not cover sit in [../assets/regression-configs.json](../assets/regression-configs.json) (JSONC that mixes the `servers`, `projects` and `mcpServers` shapes and Claude Code settings keys on purpose) and [../assets/regression-codex-config.toml](../assets/regression-codex-config.toml), one server per case. After changing the scanner, run from the skill folder:

```bash
node scripts/scan-mcp-config.mjs assets/regression-configs.json assets/regression-codex-config.toml
node scripts/scan-mcp-config.mjs assets/regression-configs.json assets/regression-codex-config.toml | grep -c demo-not-real
node scripts/scan-mcp-config.mjs assets/regression-configs.json assets/regression-codex-config.toml | grep -c $'\x1b'
```

Expected: `32 FAIL, 27 WARN, 69 INFO, 10 PASS across 54 servers in 2 files` and exit code 1, and `0` from the second and third commands: no planted value and no escape character may appear in the output, text or `--json`. Every stdio entry also gets INFO CF03, and every stdio entry whose launcher the scanner does not know gets INFO CF04 (`curl_pipe`, `chained`, `node_flag_target` in both files, `python_module`). Per server:

- `input_ref`: PASS; `${input:crm-key}` is a reference, not a secret.
- `header_key_literal`: FAIL CF01 on header `X-Key`.
- `flag_secrets`: FAIL CF01 twice (`--api-key <value>` and `--token=<value>`); INFO CF04, `example-mcp==1.2.0` pinned top-level only.
- `path_key`: WARN CF02; the key-like path segment prints as `<redacted-segment>`.
- `curl_pipe`: FAIL CF06, a download piped into a shell.
- `chained`: WARN CF06; the `&&` means the command needs a human read.
- `windows_cmd`: INFO CF06 for the `cmd /c` wrapper and WARN CF04 for the unpinned `win-mcp` inside it.
- `docker_socket`: FAIL CF08 (Docker socket mount), WARN CF08 (`--privileged`), WARN CF04 (a tag, no digest).
- `docker_isolated`: PASS, with INFO CF04 (digest) and INFO CF08 (`--network none`).
- `git_ref`: WARN CF04, a git reference without a commit.
- `inspector_old`: FAIL CF05 (CVE-2025-49596, CVE-2025-58444 and the 2026 advisories; fixed on the 1.x line in 1.0.4) and INFO CF04.
- `inspector_v2`: FAIL CF05 (2.0.0 to 2.11.0, and the 2026-11-04 advisories; fixed in 2.14.0) and INFO CF04. `inspector_v2_13`: FAIL CF05 for the 2026-11-04 advisories alone.
- `cursor_auth_secret`: FAIL CF01 on `auth CLIENT_SECRET`. `env_file_ref`: INFO CF01, the file is not opened. `sandboxed_server`: WARN CF09. `headers_helper`: INFO CF06. `plain_websocket`: FAIL CF03.
- `loopback_http`: PASS, with INFO CF03 that loopback is not authentication.
- `docker_env_literal`: FAIL CF01 on `docker -e ADS_API_TOKEN`; `-e LOG_LEVEL` passes a host variable through and is not reported; INFO CF04 and CF08.
- `header_arg_literal`: FAIL CF01 on `argument --header X-API-Key`; INFO CF04 for `mcp-remote@0.1.16`.
- `query_auth_token`: FAIL CF02 on query parameter `auth_token`.
- `dsn_arg`: FAIL CF02, a password in a `postgresql://` argument; only `postgresql://example.com:5432/marketing` and the length print.
- `dsn_env`: FAIL CF02 twice, for `DATABASE_URL` and `MONGODB_URI`, although their key names end in `_URL` and `_URI`.
- `git_userinfo`: FAIL CF02 for user information in the `git+https` spec, and WARN CF04 that prints the spec without it.
- `tarball_userinfo`: FAIL CF02, and WARN CF04 "from a URL" with the redacted spec.
- `registry_flag`: WARN CF04, packages from `npm.example.net` (`--registry`); INFO CF04 for the pin.
- `extra_index`: WARN CF04 with the dependency confusion note (`--extra-index-url`); INFO CF04 for the pin.
- `inline_env_shell`: FAIL CF01 on inline variable `EXAMPLE_TOKEN`; INFO CF06 names the launcher (`npx`), never the assignment; INFO CF04.
- `node_flag_target`: FAIL CF01 on `--token` in the JSON file, PASS in the TOML file, and WARN CF10 that shows each target as `node (arguments sha256:...)`.
- `project_latest`: WARN CF04 for `@latest`; the server is labelled by a project hash, never the path; `${EXAMPLE_API_KEY}` is a reference.
- `hubspot_pat_env`, `hubspot_pat_arg`, `meta_value_shape`: FAIL CF01, "in the HubSpot private app token format" or the Meta one, with only the label and length printed. The shape counts whatever the key name; `--pat` also counts by name now.
- `name_at_tarball`, `name_at_git`: FAIL CF02 on the package URL, and WARN CF04 that prints the URL without its user information. `pep508_git`: the same for `name @ git+https://...`, spaces included.
- `sse_base64_path`: FAIL CF02 for + and = in the path, WARN CF03 for SSE; the segment prints as `<redacted-segment>`. `short_path_key`: WARN CF02, 16 or more characters that mix letters and digits. `query_bare_key`: WARN CF02 for a query with no parameter name, printed as `<redacted-name>`.
- `oauth_json_secret`: FAIL CF01 on `oauth clientSecret`.
- `uv_tool_run`, `uv_run_with`: WARN CF04 with no version, read like uvx. `python_module`: PASS with "stdio; pinning not checked" and INFO CF04.
- `remote_no_auth`: PASS with "HTTPS; no credential found in the file".
- A server name holding ESC[8m: PASS, printed as `escU+001B[8mhidden`.
- Top of the JSON file: WARN CF09 for `enableAllProjectMcpServers`, WARN CF09 for `permissions.defaultMode` "auto", WARN CF09 for `mcp__crm__*` (whole server), WARN CF09 for `mcp__ads__update_budget` (name says it writes), INFO CF09 for the read tool rule, INFO CF09 that the deny rule with parentheses is skipped, INFO CF09 for `enabledMcpjsonServers`. The `env` block is never read.
- `inline_table_secret`: FAIL CF01 on `ALPHA_TOKEN` inside an inline table; the multi-line array with a comment still parses (INFO CF04).
- `static_header`: FAIL CF01 on a static `http_headers` value.
- `env_header_ref`: WARN CF09 for `auto`; `env_http_headers` holds a variable name, so no CF01.
- `per_tool_mode`: WARN CF09 for `tools.delete_all.approval_mode = "approve"`; `bearer_token_env_var` holds a name, so no CF01.
- `header_helper`: PASS, with INFO CF06 for `http_headers_helper`; its triple-quoted line is INFO CF11, outside the subset.
- `legacy_bearer`: FAIL CF01 on the server-level key `bearer_token`. `oauth_toml_secret`: FAIL CF01 on `oauth client_secret`.

[../assets/regression-settings.json](../assets/regression-settings.json) holds Claude Code settings keys with no `permissions.defaultMode`:

```bash
node scripts/scan-mcp-config.mjs assets/regression-settings.json
```

Expected: `0 FAIL, 0 WARN, 2 INFO, 0 PASS across 0 servers in 1 file` and exit code 0. One INFO CF09 says interactive sessions on v2.1.283 or later start in auto mode, which is condition C15 for write-capable servers; the other is the read tool rule.

[../assets/regression-unparseable.txt](../assets/regression-unparseable.txt) is broken JSON with a fake token next to the error. Both scripts must exit 2 and print only `near line 4, column 59`:

```bash
node scripts/scan-mcp-config.mjs assets/regression-unparseable.txt
node scripts/review-mcp-tools.mjs assets/regression-unparseable.txt
```
