Two Locks and a Pipe: A Local-First Outreach Engine Your Own AI Agent Can Drive

Published on 28 September 2026
8 min read
agent-orchestration
Two Locks and a Pipe: A Local-First Outreach Engine Your Own AI Agent Can Drive

TL;DR

  • What it is — an outreach engine (contacts, sequencing, deliverability, reply matching, an approval queue and a dashboard) that runs entirely on your own machine.
  • Who holds the data — you do. Your mailbox, your browser session, your keys. No account, no telemetry, and your prospects’ details never leave the laptop.
  • Two locks before anything sends — a human approves each message, and a global switch has to be armed. It ships disarmed.
  • Every draft shows its work — model, confidence, source URLs, and who wrote it. A guess is visible as a guess before you approve it.
  • Your own AI agent can drive it — 20 tools over MCP on stdio. It can research and draft; approving and arming are refused in code, not in a prompt.
  • What it costs — $0.003 an email, under $1 in a realistic month, a $3 hard cap. Zero on a local model.
  • Where it is — gitlab.com/sugix/outreach, on OpenWorker (MIT) and OpenClaw.

Table of Contents

  1. What it looks like
  2. Apollo is not an AI product
  3. Two locks
  4. Every draft shows its work
  5. It refuses to write to people who cannot act
  6. It checks the thing everyone forgets
  7. Built on OpenWorker and OpenClaw
  8. Your own AI agent can drive it
  9. Bring your own keys
  10. What it costs to run
  11. Get it

The list of people you most want to reach is also the list you should be most careful with. Investors. The operators whose feedback would change your roadmap. The design partner who might become your first customer. None of them agreed to be uploaded to a vendor’s cloud so you could email them.

Every outbound tool we tried asks for exactly that, and charges for it. At Cognilogue we build Hexr, whose whole argument is that evidence stays inside the infrastructure that produced it. Shipping our own prospects somewhere else to sell that would have been a poor argument.

So we built the engine we wanted, and we are open-sourcing it.

What it looks like

The approval queue: sending disarmed, least-confident draft at the top.

That is the product in one screen — an unapproved draft, an amber box saying the model cited no sources and scored itself 0.52, and a badge confirming nothing can leave this machine yet.

Apollo is not an AI product

Strip the marketing and what remains is a system of record plus a deterministic send engine, with generation bolted on. The AI is maybe 5% of the value. The other 95% is unglamorous:

  • deduplication that holds across email, LinkedIn URL and domain
  • a sequencer that fires step 2 four days after step 1 unless they replied
  • threading, so the follow-up lands in the same conversation
  • suppression, so an unsubscribe is permanent and a bounce is never retried
  • per-mailbox caps, warm-up ramps, and send windows in the recipient’s timezone

Get sequencing wrong and you email someone who already answered. Get suppression wrong and you damage a sending domain you cannot buy back.

Verified is confirmed deliverable; guessed is blocked from sending outright.

The model writes text. Code decides what leaves the machine. Everything touching deliverability or a third party is deterministic Python under test. The model is used in three places where judgement beats rules: researching targets, writing one personalised line, and classifying a reply.

Two locks

draft written → [ LOCK 1 — a human approves this message ] → approved
                                                                  ↓
                        [ LOCK 2 — sending is ARMED ] → actually sent

Both must be open, and the second is off by default. Source a hundred contacts, draft every message, read them all and change your mind — nothing can escape.

Two locks rather than one, because approving fifteen drafts on a Sunday evening should not mean fifteen emails leave while you are still reading the third.

The gate is on the direction, not the tool: an agent may always disarm. Stopping something should never require permission.

Every draft shows its work

The interesting failure mode of AI outbound is not a clumsy sentence. It is a confident, specific, false claim about the person you are writing to.

Provenance on a draft written by an external agent.

So the writer must return a confidence score and the source URLs behind its claim. Both are stored, and the queue sorts least-confident first.

Authorship is a column too — writer, agent, human, human_edited — so a line you wrote is never mistaken for the model’s, and the model’s confidence never attaches to your words.

It refuses to write to people who cannot act

Job titles go stale. Enrichment providers happily return “Managing Partner” for someone whose own firm’s page says Partner Emeritus, not participating in firm business.

So the writer is asked, before it composes anything, whether this person can act on this ask:

enrollment 5: paused — not targetable: the firm's own team page lists this
person as 'Partner Emeritus', an advisory-only role, not someone who can act
on a funding ask.

Same principle elsewhere:

  • an unconfirmed address is refused at enrolment, with the command that would confirm it
  • a pattern-guessed address is never mailed at all
  • two concurrent ticks cannot queue the same message twice — an atomic claim stops the second, and a unique partial index makes a duplicate impossible anyway

It checks the thing everyone forgets

deliverability for yourdomain.com:
  ✅ MX     1 smtp.google.com
  ❌ SPF    no SPF record — cold mail will be filtered as spam
  ❌ DMARC  no DMARC record

No amount of personalisation survives a missing SPF record. Two DNS records, five minutes, free — and no tool we tried mentioned it once.

When something fails, the error names the real cause. A blocked port reports a network block and the ports it tried, instead of sending you to regenerate a working credential.

Built on OpenWorker and OpenClaw

We did not want to write an SSRF guard or a secret store from scratch. OpenWorker (MIT) supplies five modules, vendored with their upstream SHA in a header comment so they stay diffable:

Module What it handles
SecretStore 0600 JSON, ${ENV} indirection, atomic writes via mkstemp + os.replace — the naive version leaves plaintext at umask 0644 for the length of the write
AuditStore append-only schema with redaction of anything matching token / secret / password / api_key, and of message bodies
Model client one OpenAI-compatible client for DeepSeek, Kimi, Qwen and Ollama, including tool-call salvage for local models that emit calls as XML
SSRF guard refuses loopback, RFC1918, link-local and CGNAT, checks every redirect hop, and pins each connection to the vetted IP so a rebinding DNS record cannot swap targets
IMAP/SMTP server presets, MIME decoding, IMAP SEARCH construction

OpenClaw supplies the browser — a real, signed-in Chrome over CDP, with its own managed profile, so a research session is isolated from everything else.

Ours is the database, sequencing, deliverability, reply matching, suppression, the approval queue, the dashboard, the personas and the agent interface: 6,700 lines of Python and 87 tests.

Your own AI agent can drive it

The engine exposes 20 tools over MCP on stdio — a pipe between two processes on one machine. It opens no port, so there is nothing to reach remotely, by construction.

claude mcp add outreach -- /path/to/.venv/bin/outreach-mcp

Read: status, contacts, approvals, replies, metrics. Build: research firms, find and verify emails, draft. Send-adjacent: approving and arming, both refused by default.

> decide_approval(approval_id=11, approve=true)
refused        approving a draft is reserved for a human
why            this action decides whether a real person receives a message
how_to_allow   outreach agent-permissions --agent-may-approve

That refusal is the first statement in the function, not a line in a prompt, so it cannot be argued with.

The tool we did not anticipate is draft_message. A writer persona only knows a positioning file; an agent inside your codebase knows the architecture and the real proof points. So it writes the email — into the approval queue, sources required, marked authored_by: agent.

Bring your own keys

Four things, and you hold the credential for each. Only the first is unavoidable:

Credential What it unlocks Needed
Your mailbox Sending, and reading replies over IMAP — a Google app password required
A model Research, drafting, triage. DeepSeek, Kimi or Qwen by key — or Ollama on localhost for nothing for the AI parts
Hunter Finding and verifying addresses. Without it, import a CSV optional
Web search DuckDuckGo needs no key; Tavily or Brave if you have one keyless

Swapping the model is a line of configuration. The same OpenAI-compatible client drives all four, with the same governance either way.

What it costs to run

Every model call, with tokens and cost.

One personalised email $0.003
Follow-up steps 2 and 3 $0
10 firms → 40 named decision-makers 11 searches
A realistic month under $1
Hard monthly cap, enforced before any call $3

The AI is the cheapest part of an AI outreach tool. The expensive parts are a sending domain you cannot repair and an hour of your own judgement.

Get it

git clone https://gitlab.com/sugix/outreach && cd outreach
python3.11 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/outreach init
.venv/bin/outreach doctor     # keys, IMAP/SMTP, sending-domain DNS, model ping
.venv/bin/outreach serve      # dashboard on 127.0.0.1

About an hour, most of it waiting on Google to issue an app password. docs/ONBOARDING.md walks through the rest; AGENTS.md is written for the agent rather than for you.


Agents doing consequential work is the problem Hexr exists for: not whether the model was right, but who authorised the action and whether you can prove it. This engine is that argument, running on something that mattered to us.