Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Test matrix. No secrets, no credentials, no network services: the Codex
# tests use a scripted CLI or localhost mocks. The Codex provider supports
# macOS and Linux only; on Windows the suite covers Claude and checks that
# Codex is refused (Codex-only test modules skip themselves there).
name: tests

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install
run: python -m pip install -e ".[dev]" "setuptools>=68" wheel
- name: Unit, integration, end-to-end and soak tests
run: python -m pytest -q
- name: Installed wheel (scripted Codex CLI on PATH)
if: runner.os != 'Windows'
env:
HARVEY_PACKAGE_TESTS: "1"
run: python -m pytest -q tests/test_packaging.py
- name: Real Codex CLI 0.159.3 against localhost mocks (no credentials)
if: runner.os != 'Windows'
env:
HARVEY_CODEX_CLI_TESTS: "1"
run: |
npm install -g @openai/codex@0.159.3
python -m pytest -q tests/test_codex_cli_surface.py
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ Notable changes to Harvey. Dates are release dates; the format follows
follow [semver](https://semver.org/), with the caveat that Harvey is pre-1.0 and
minor versions can still change behaviour.

## [Unreleased]

### Added

- **Opt-in Codex brain (ChatGPT plan).** Set `brain.provider: codex` (or pick
it in `harvey setup`, or `harvey train --provider codex`) to think with the
Codex CLI on a ChatGPT subscription instead of Claude. Claude remains the
default and there is no automatic fallback between providers. Each call
runs a throwaway, isolated `codex app-server` with no tools; Harvey verifies
the lockdown from Codex's own reports before sending any prompt, aborts on
any attempted tool use, refuses API-key logins and custom endpoints
(checked before your login is exposed to Codex), and never changes your own
Codex settings or login: each call gets a private copy of the login without
its refresh token, so an expired login must be renewed with Codex itself or
`codex login`. Requires Codex CLI 0.159.3; other versions are refused. See
the README section "Using a ChatGPT plan instead (Codex)".
- `brain.model` picks the model for either provider, for Harvey only. For
Codex use `gpt-5.5` (suggested by `harvey setup`): the verified text-only
model. Blank auto-selection only picks listed models without code-execution
tools, and on current ChatGPT accounts there are none; code-mode models are
refused.
- If Codex can't be used (expired or renewal-due login, unsupported CLI,
unsafe Codex config, rejected model, plan usage limit), Harvey pauses the
heartbeat instead of running agents that would fail: replies stay unread
and unescalated, queued emails wait, and the reason shows in the log,
`harvey status` and the dashboard. It resumes on its own once the cause is
fixed. `harvey status --check` runs the same check (no prompt sent) on demand
and exits non-zero when it fails. The Codex provider supports macOS and
Linux; it is refused on Windows (use Claude there).

### Changed

- Each heartbeat now sends email after the agents that read and answer
replies have finished, and native mail providers (Gmail/SMTP) read the
mailbox every cycle, so a reply always cancels its queued follow-ups before
that cycle's sends (previously the two could run at the same time, and a
reply arriving with no open conversation waited for a later cycle). This
applies to Claude and Codex alike.
- A reply is only recorded once its reply draft (if it needs one) is
written, so a brain outage part-way never half-records it. If a working
brain returns no usable draft, the conversation is flagged for a human
instead of being left open with no answer.
- Usage is tracked per provider. The budget gate, `harvey usage`, `harvey
status` and the dashboard read and label the active provider's own quota;
existing usage history is kept and attributed to Claude.

## [0.2.1] — 2026-10-13

A security release. Update if you run Harvey.
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ People will ask "how does this work?" — explain it simply:

- **"What does it cost?"** → Just your Claude Max subscription (which you already have). The only paid integration is Instantly for sending emails (their cheapest plan works). Everything else — prospecting, email writing, reply handling — is included.

- **"Can it use ChatGPT / Codex instead of Claude?"** → Yes, opt-in. Install Codex CLI 0.159.3 (`npm install -g @openai/codex@0.159.3`; other versions are refused), run `codex login` with ChatGPT, and set `brain: { provider: codex, model: gpt-5.5 }` in harvey.yaml (or choose codex in `harvey setup`, which suggests gpt-5.5, or `harvey train <url> --provider codex --model gpt-5.5`). gpt-5.5 is the verified text-only model; a blank model fails on current accounts because the live catalog hides it, and code-mode models are refused. Claude stays the default; Harvey never switches providers on its own. Each Codex call runs isolated with no tools — see "Using a ChatGPT plan instead (Codex)" in the README. macOS and Linux only; on Windows, Codex is refused, so use Claude. Don't edit the user's own `~/.codex` config to make it work.

- **"What's the dashboard?"** → Run `harvey dashboard` to see a local web UI at localhost:5555. It opens on **Today** — anything waiting on a decision, then the pipeline. Tabs for Signals, Companies, Contacts, Campaigns, Outbox, Conversations, Activity, Usage, Settings.

- **"What are signals?"** → Signals are the facts Harvey collects about a business: who its current agency is, whether it's running ads, whether it has online booking, how it ranks. **Harvey proposes; you confirm.** Nothing is collected until the user says yes on the Signals tab (or `harvey signals --confirm ...`). That keeps spend intentional and makes every prospect list explainable — a cohort is a query over signals a human chose, not a black box.
Expand Down Expand Up @@ -278,5 +280,6 @@ harvey sending pause|resume # Kill switch for all outbound
- **"externally-managed-environment"**: Use a venv, not system Python
- **SQLite errors**: The `data/` directory is created automatically on first run
- **Claude headless mode fails**: User needs `claude login` and an active Max subscription
- **Codex calls fail** (`brain.provider: codex`): the log line says why — `codex login` with ChatGPT (also when the login "has expired or is due for renewal": Harvey never renews it), install the supported Codex version (`harvey status --check` shows what's wrong without a model call; while Codex is unusable Harvey pauses whole cycles on purpose), pick a `brain.model` without code mode, or remove whatever Codex config layer is forcing tools/hooks/MCP back on. Never weaken Harvey's isolation checks to get past it.
- **Instantly API 401**: Wrong API key or needs the Growth plan for API access
- **Google search rate limiting**: Harvey automatically falls back to DuckDuckGo and Bing. For reliable search, add a Serper API key ($5/mo).
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ anything, and approving mail one message at a time:
- [The dashboard](#the-dashboard)
- [How the data is stored](#how-the-data-is-stored)
- [Configuration](#configuration)
- [Using a ChatGPT plan instead (Codex)](#using-a-chatgpt-plan-instead-codex)
- [Legal and deliverability](#legal-and-deliverability)
- [Troubleshooting](#troubleshooting)
- [Project structure](#project-structure)
Expand Down Expand Up @@ -109,7 +110,7 @@ Profiling and outbox draining ride along on **every** cycle regardless, because
### Prerequisites

- **Python 3.11+** — check with `python3 --version`. macOS ships 3.9; if that's what you have, `brew install python@3.13` first.
- An active **Claude Pro or Max** subscription, with the CLI logged in (`claude login`)
- An active **Claude Pro or Max** subscription, with the CLI logged in (`claude login`) — or, opt-in, a **ChatGPT plan with the Codex CLI** ([see below](#using-a-chatgpt-plan-instead-codex))
- A mailbox to send from — Gmail/Workspace recommended, on a *dedicated secondary domain*

### Easiest: let Claude set it up
Expand Down Expand Up @@ -396,8 +397,11 @@ channels:
email: { provider: gmail | smtp | instantly, max_daily_sends, require_approval,
send_to_risky, max_bounce_rate }
usage: { max_daily_claude_percent, heartbeat_interval_minutes, quiet_hours }
brain: { provider: claude | codex, model, reasoning_effort } # optional; claude by default
```

`max_daily_claude_percent` caps whichever subscription `brain.provider` uses (the name predates Codex support).

If `harvey.local.yaml` exists it wins. It's gitignored, so a fork can carry real product configuration while the tracked `harvey.yaml` stays a template — nobody publishes their positioning and pricing by accident.

### Commands
Expand All @@ -408,15 +412,47 @@ harvey dashboard # web UI at localhost:5555
harvey signals # review/confirm what to prospect against
harvey discover # find businesses; --providers / --estimate
harvey profile # read discovered companies' sites (free)
harvey train <url> # learn a product from its website
harvey train <url> # learn a product from its website (--provider codex --model gpt-5.5)
harvey status # pipeline summary
harvey usage # Claude quota + per-agent tokens
harvey usage # active provider's quota + per-agent tokens
harvey outbox # review queued email
harvey export # deliverable prospects → CSV
harvey sending pause|resume # kill switch
harvey gmail auth # one-time Gmail OAuth
```


## Using a ChatGPT plan instead (Codex)

Claude is the default and needs no change. If you'd rather spend a ChatGPT plan (Plus, Pro, Business, …), Harvey can think through the [Codex CLI](https://developers.openai.com/codex) instead — still a subscription, never per-token API billing.

```bash
npm install -g @openai/codex@0.159.3 # the Codex version Harvey has verified (see Requirements)
codex login # choose "Sign in with ChatGPT" — API-key logins are refused
```

Then either pick **codex** in `harvey setup` (it suggests `gpt-5.5`), run `harvey train <url> --provider codex --model gpt-5.5`, or add to your config:

```yaml
brain:
provider: codex
model: gpt-5.5 # the verified text-only Codex model (see Models)
reasoning_effort: "" # optional: minimal | low | medium | high | xhigh
```

What to know:

- **It's one or the other.** Harvey uses exactly the provider you configure. If Codex fails, the call fails with a clear log message; it never falls back to Claude (or to an API key). A Codex-only machine doesn't need Claude installed.
- **Your Codex setup is left alone.** Each call runs throwaway `codex` processes in a private temporary home and empty workspace, so your `~/.codex/config.toml`, MCP servers, plugins, hooks, skills, `AGENTS.md` and memories are never loaded, and Harvey's model choice never changes your own Codex default.
- **No tools, verified before every prompt.** Prompts contain untrusted text (prospect replies, scraped pages), so the model must have nothing to act with. Harvey disables every tool-providing Codex feature, web search, sub-agents, hooks, skills and project instructions, starts the thread with no execution environment, and then checks Codex's own reports (effective config, account type, thread settings, MCP status, hooks) before sending anything. Anything it can't confirm stops the call. During the turn, any attempted tool call, approval request or non-text item kills the runtime. A read-only sandbox is also set, as extra protection only.
- **Your login only goes to OpenAI.** Before your login is put into the runtime at all, Harvey starts Codex without it and checks that requests would go to the built-in OpenAI provider and the default ChatGPT endpoint. Any custom provider, overridden built-in provider, base URL, telemetry exporter or other endpoint in any Codex config layer (for example `/etc/codex/config.toml` or a managed config) stops the call there.
- **Login handling.** Harvey supports Codex's file credential storage (`~/.codex/auth.json`, or `$CODEX_HOME/auth.json`, mode `600`): the default unless you've configured an OS keyring. Keyring storage isn't supported. On every call Harvey reads that file (read-only, never writing or moving it) and gives the runtime a private copy *without the refresh token*, deleted after the call. Codex therefore can't change or renew your real login. When the login has expired, or is due for Codex's routine renewal (after about 8 days), Codex calls stop with "Your Codex login has expired or is due for renewal". Use Codex once yourself (it renews the login automatically) or run `codex login`, and Harvey picks up the renewed login on its next call. Harvey never renews it for you.
- **Models.** Use `model: gpt-5.5`. It's the model Harvey has verified as text-only with Codex 0.159.3 (no code mode, no extra tools), and it works on ChatGPT plans even though Codex's live catalog hides it from the model picker. Codex's newer "code mode" models (`gpt-6-*`, `gpt-5.6-*`) always carry built-in code-execution tools that no setting removes, so Harvey refuses them. If you leave `model` blank, Harvey only auto-selects a model the catalog *lists* and that has no code mode or extra tools. On current ChatGPT accounts no listed model qualifies, so a blank `model` fails with an error naming the hidden models you can set explicitly, plus each model's reason. If you set `model`, Harvey uses exactly that model or fails; it never substitutes another. Naming a code-mode model gives an error listing the models that work.
- **Quota.** The budget gate and dashboard read your ChatGPT plan's real rate-limit windows from Codex, for the limit bucket that applies to your model, and compare them to `usage.max_daily_claude_percent`. If Codex reports that usage is blocked, or a call fails on the plan's usage limit, Harvey treats the plan as full (even when no window is reported) and stops Codex calls for an hour. If the limits can't be read, the dashboard and `harvey usage` say "Quota unavailable" and the budget falls back to Harvey's own daily call count. Usage is recorded per provider; Codex calls are logged with tokens but no dollar cost, because the plan isn't billed per token.
- **When Codex can't be used, Harvey pauses rather than guessing.** Before each heartbeat, Harvey checks everything a call needs except the prompt itself: the Codex CLI version, the isolation, your login (expired or due for renewal), your ChatGPT account, and that your configured model can run with no tools. No prompt is sent, so nothing is used from your plan beyond a quick account check. If something is wrong, the whole cycle is skipped: no reply is read or marked handled, nothing is drafted or sent. The log line, `harvey status`, and the dashboard's Actions card say "Codex paused" and why. If Codex stops working part-way through a cycle (for example the plan hits its usage limit), the reply being handled is left exactly as it was for a clean retry, not escalated, half-recorded or lost, and that cycle sends nothing. Fix the cause (usually `codex login`), and Harvey resumes on its next heartbeat: a renewed login or reinstalled CLI is noticed immediately, a usage-limit pause ends after an hour, and other fixes are picked up within 30 minutes. `harvey status --check` runs the same check on demand and exits non-zero if it fails. Brief failures (a crash or timeout) just skip one cycle. Harvey never falls back to Claude or an API key.
- **Requirements.** **macOS or Linux**, Codex CLI **0.159.3** exactly, and Node.js for its npm package. **Windows isn't supported for Codex:** Harvey refuses it there before reading your login; use the Claude provider on Windows. Harvey checks the Codex version before every call and refuses any other version, including newer ones, because a different Codex can add tools Harvey's lockdown doesn't know about. Support for a new version comes with a Harvey update. Live use with a real ChatGPT account has been verified on macOS; Linux uses the same code and is covered by Harvey's tests, but hasn't yet been run against a real account. The Docker image ships the Claude CLI only, so Codex isn't supported in Docker. To re-verify the lockdown offline against your installed CLI, with no credentials or network: `HARVEY_CODEX_CLI_TESTS=1 pytest tests/test_codex_cli_surface.py`.
- **Overhead.** Each call checks the CLI version, starts Codex once without your login to verify its configuration, reads the model catalog, then starts it again to run the prompt; each heartbeat's check does the same without the prompt. That adds a few seconds per call, and one Brain's Codex calls run one at a time.

---

## Legal and deliverability
Expand Down Expand Up @@ -455,6 +491,8 @@ Harvey automates outreach, but **you are the sender.** Cold email is legal in mo

**Claude headless mode fails** — `claude login`, and confirm the subscription is active. Test with `claude -p "say hi"`. In Docker, mount `~/.claude` into the container.

**Codex calls fail** — Read the log line; every Codex error says what to do. "Not logged in" or "expired or is due for renewal" → use Codex once or run `codex login` with ChatGPT. "Hasn't been verified with Harvey" → install the supported Codex version named in the message. "Isolation could not be verified" → a Codex config layer outside your `~/.codex` (for example a managed or `/etc/codex` config) forces a tool, hook, MCP server, instructions or custom endpoint back on; Harvey refuses to send prompts until it's gone. "Always exposes tools" or "no Codex model is available" → set `brain.model: gpt-5.5` (the message lists each model and why). Harvey never switches to Claude on its own.

**Discovery finds nothing** — Confirm signals first (`harvey signals --confirm free`); nothing is collected until you do. If you're on the free OpenStreetMap source, it only covers mapped trades and is thin for service-area businesses — `harvey discover --providers` shows the alternatives.

**Overpass is throttling** — It's free volunteer infrastructure. Harvey walks three mirrors before giving up. Wait a few minutes, or use a paid provider for bulk work.
Expand Down
Loading
Loading