5.4 · The Tool Layer

5.4 · 25 min

The Tool Layer

Here is the fastest way to end a consulting engagement badly: wire your report agent to Meridian's HubSpot with the owner's personal API key, because it was the key sitting in the doc. Now a worker built to read analytics can rewrite every deal, delete contacts, and export the whole CRM, and the injection you have not defended against yet (lesson 5.7) has the run of the building. The Tool layer connects agents to systems of record, and the entire discipline of it is one word from lesson 3.8: least privilege. An agent gets exactly the access its process spec requires, scoped and read-only until write is earned, and not one permission more.

The access matrix

Before you connect anything, you draw the matrix: every worker down one axis, every system across the other, and in each cell the narrowest access that worker's process spec actually needs. This is 3.8's read-target boundary as a planning artifact. It is the document that stops "it needs HubSpot" from silently meaning "it gets all of HubSpot."

SystemReport DrafterLead TriageMeeting-to-Tasks
Analytics stand-in data (GA4/Ads CSV)readnonenone
Knowledge repo (SOPs, voice, client files)readreadread
CRM (HubSpot contacts/deals)noneread one contactnone
Review folder (drafts out)writewritenone
Fathom transcriptnonenoneread one transcript
Asananonenonewrite to a draft/staging project
Email (Gmail send)nonenonenone

Read the matrix and the security posture is visible at a glance. No worker can send email, because none of them is graduated past Draft. The report drafter cannot touch the CRM at all, because its spec never needs to. Lead triage reads one contact, the one the inbound lead maps to, not the contact database. Every "none" is a deliberate denial, and every "write" is scoped to a draft or staging surface, never a live client-facing send. When lesson 5.7's injected lead tries to make the triage worker exfiltrate the client list, the matrix is why it cannot: triage has no read access to other clients' data, so there is nothing to exfiltrate even if the model is fooled.

MCP vs CLI vs API, per connection

Each connection in the matrix is made one of three ways, and the choice is concrete, the same tier logic as lesson 2.7 pointed at business systems.

  • MCP server. The default for a system that has a maintained one. An MCP server exposes a system's operations as tools with typed inputs, and Claude Code connects to it with scoped credentials. As of July 2026 there are official, maintained MCP servers for the systems Meridian runs on: HubSpot's remote MCP server exposes read and write across CRM objects (contacts, companies, deals, tickets) and engagements, with organizational and marketing context read-only; Notion offers a hosted MCP server reached over OAuth. Use the vendor's official server where one exists, and verify it is current and maintained before you trust it (2.8), because a stale or third-party server is untrusted code in your tool layer.
  • CLI. Right when the system's command-line tool is the cleanest interface and you can scope it with permission rules. Git is the archetype (the 2.11 changelog agent used Bash(git log:*)); for a business, a well-scoped CLI wrapped in an allow rule is a fine read-only connection when no good MCP server exists.
  • Direct API (custom tool). Right when you need a specific, narrow operation the MCP server does not expose, or when the "system" is your own stand-in data. You wrap the call in a custom SDK tool (2.11's tool() and createSdkMcpServer()), which gives the agent exactly one function with a typed schema and nothing else. This is the tightest possible scope: the agent cannot call an operation you did not write.

The bias order is MCP for breadth where a maintained server exists, CLI for a scoped read where a command tool is cleanest, custom API tool for a single narrow operation or your own data. Whatever the mechanism, the access it grants must match the matrix cell, not the vendor's full surface.

Credentials: scoped tokens, never master keys

The credential a worker runs with is the real boundary, because a permission rule you forgot is only backstopped by what the token itself can do. Three rules, all from 3.8, all non-negotiable:

  • Scoped tokens, never master credentials. The report drafter gets a token that can read analytics and nothing else. It never gets the owner's login or a full-access API key. If the vendor supports scoped tokens (HubSpot private-app tokens with selected scopes, a Google service account with read-only analytics scope), use the narrowest scope that covers the process spec.
  • Read-only first, always. A new connection is read-only until the worker's output has earned write, the same way the worker itself is on Draft until it earns Approve. Write access and autonomy graduate together, on evidence.
  • Secrets out of the artifact. Tokens live in environment variables or a secrets manager, never in the prompt, the knowledge repo, or the code. The .env-blocking hook from 2.6 and 3.8 rides along here so a worker cannot read its own credential file and leak it, the exact exfiltration path lesson 5.7 will attack.

In practice: read-only, scoped, against stand-in data

You do not develop an Agent OS against a client's live systems, you develop against stand-in data that mirrors them, so a bug during the build cannot touch a real client. For Meridian you set up a stand-in directory and connect the report drafter to it read-only, using the tightest mechanism (a scoped file read plus a custom tool for the CRM lookup).

tool layer for the report drafter (access surface only)typescript
// The report drafter's entire tool surface, matching its matrix row:
// read analytics stand-in data, read the knowledge repo, write to a
// review folder. No CRM, no email, no write anywhere else.
options: {
cwd: meridianRoot,
settingSources: [],            // inherit nothing; 2.11 discipline
permissionMode: "dontAsk",     // unattended: unresolved => denied
allowedTools: [
  "Read(standin-data/**)",     // this week's GA4/Ads CSVs (read)
  "Read(meridian-knowledge/**)", // SOPs, voice, client files (read)
  "Write(review/**)",          // drafts out to the review folder only
  "Grep", "Glob",
],
// A scoped .env-blocking PreToolUse hook (2.6/3.8) rides here so the
// worker cannot read any credential file even inside standin-data.
}

That surface is the matrix row turned into enforced configuration. The worker can read exactly two trees and write exactly one, and dontAsk denies everything else with no human to ask. When you later swap the stand-in CSVs for a real analytics MCP connection, the shape does not change: read-only, scoped to this client, backstopped by a narrow token. The stand-in is not a toy, it is the same tool layer with safer data behind it.

Lablab-5-4Wire two least-privilege connections for Meridian

Goal: Build the report drafter's read-only tool surface against stand-in data, prove it cannot exceed its access matrix row, and provision a scoped credential pattern.

Prereqs: the meridian-knowledge repo from 5.3, the changelog-agent SDK setup from 2.11 (for the query() pattern and a working ANTHROPIC_API_KEY).

  1. Create a standin-data/ directory next to the knowledge repo. Add a stand-in northwind/ga4-week.csv and northwind/ads-week.csv with a handful of plausible rows (sessions, leads, spend, CPL). This is your safe development data.
  2. Write the access matrix for all three planned workers as meridian/access-matrix.md. Every cell is read, write, or none, and each non-none cell cites the process-spec line that justifies it. No cell is granted "just in case."
  3. Build connection one: a scoped file read. Configure a query() options object (from 2.11) whose allowedTools grants only Read(standin-data/**), Read(meridian-knowledge/**), Write(review/**), Grep, Glob, with settingSources: [] and permissionMode: "dontAsk".
  4. Build connection two: a custom tool for a CRM lookup. Using 2.11's tool() and createSdkMcpServer(), write a lookupContact(email) tool that reads a stand-in standin-data/crm.json and returns only one contact's public fields. This is the tightest scope: the agent can look up one contact and cannot enumerate the database.
  5. Prove the boundary. Run the drafter with a prompt that also instructs it to read standin-data/crm.json directly and to write outside review/. Confirm the direct CRM read and the out-of-folder write are both denied (only the lookupContact tool can touch CRM data, and writes resolve only under review/).
  6. Write meridian/credentials.md: for each real connection you would make in production (analytics, HubSpot, Notion), name the scoped-token mechanism (service account scope, HubSpot private-app scopes, Notion OAuth) and state read-only-first. No real secrets; this is the provisioning plan.

Verify

  • access-matrix.md covers all three workers x the systems, every non-none cell justified by a spec line, and at least one "none" you can explain as a deliberate denial.
  • The drafter runs against stand-in data and produces a draft under review/ from the CSVs and knowledge repo alone.
  • The step-5 sabotage is denied: the direct crm.json read and the out-of-review/ write do not happen; only lookupContact can reach CRM data.
  • credentials.md names a scoped-token mechanism per real system and states read-only-first; no master key appears anywhere.
>Troubleshooting
  • The drafter could read crm.json directly in step 5: your allowedTools granted Read(standin-data/**) broadly, which includes crm.json. Narrow the read rule to the analytics subpath (Read(standin-data/*/ga4-week.csv) and the ads file) so CRM data is reachable only through the custom tool, matching the matrix.
  • The custom tool returns the whole CRM: your lookupContact handler read and returned all of crm.json. Filter to the single matched contact and return only the fields the process needs; a tool that returns everything is not least privilege.
  • You put a real token in credentials.md to "test it": stop. The plan names mechanisms, not secrets. Real tokens live in env vars or a secrets manager, and the .env hook exists so a worker cannot read them.

Knowledge check

Knowledge check

Q1A teammate wires the report drafter to HubSpot using the founder's personal full-access API key, reasoning that 'it only reads, so the extra access is harmless.' Why is this wrong per the Tool layer's discipline?
Q2You are connecting the Meeting-to-Tasks worker to Asana. Its matrix row says 'write to a draft/staging project.' Which connection and credential choice fits the Tool layer's bias?
Q3Why does the Tool layer develop the report drafter against a standin-data/ directory instead of Meridian's live GA4 and HubSpot from day one?

Sources

  • Model Context Protocol in Claude Code (connecting external systems as scoped tools, server configuration and scopes; verified July 2026): https://code.claude.com/docs/en/mcp (fetched July 2026)
  • Agent SDK configure permissions (allow-rule scoping such as Read(path/**) and Write(path/**), dontAsk denying unmatched calls; consistent with 2.11): https://code.claude.com/docs/en/agent-sdk/permissions (fetched July 2026)
  • HubSpot MCP server (remote server access surface: read/write across CRM objects and engagements, read-only organizational and marketing context; confirmed maintained and current, July 2026): https://developers.hubspot.com/mcp (fetched July 2026)
  • Notion MCP (hosted server reached over OAuth for workspace read/write; confirmed current, July 2026): https://developers.notion.com/docs/mcp (fetched July 2026)