Every Bento feature, plus the ones their own CLI does not have, plus a local SQLite store that turns 'who are my top spenders by tag' into a one-liner.
A Go binary CLI for Bento email marketing that covers all 39 features from Bento's official CLI and MCP, brings in transactional send + spam validation + segments that are SDK-only or docs-only today, and layers 12 novel commands on top -- snapshot diff, hygiene pipeline, Vendure purchase replay, churn-risk scoring, broadcast what-if. Offline FTS over your subscribers via SQLite, --json on every command, MCP tools auto-derived from the Cobra tree.
Created by @bossriceshark (bossriceshark).
The recommended path installs both the bento-pp-cli binary and the pp-bento agent skill (Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, and other agents supported by the upstream skills CLI) in one shot:
npx -y @mvanhorn/printing-press-library install bentoFor CLI only (no skill):
npx -y @mvanhorn/printing-press-library install bento --cli-onlyFor skill only — installs the skill into the same agents as the default command above, but skips the CLI binary (use this to update or reinstall just the skill):
npx -y @mvanhorn/printing-press-library install bento --skill-onlyTo constrain the skill install to one or more specific agents (repeatable — agent names match the skills CLI):
npx -y @mvanhorn/printing-press-library install bento --agent claude-code
npx -y @mvanhorn/printing-press-library install bento --agent claude-code --agent codexIf npx isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.4 or newer):
go install github.com/mvanhorn/printing-press-library/library/marketing/bento/cmd/bento-pp-cli@latestThis installs the CLI only — no skill.
Download a pre-built binary for your platform from the latest release. On macOS, clear the Gatekeeper quarantine: xattr -d com.apple.quarantine <binary>. On Unix, mark it executable: chmod +x <binary>.
Install the CLI binary first. The installer writes binaries to a per-user managed bin directory by default: $HOME/.local/bin on macOS/Linux and %LOCALAPPDATA%\Programs\PrintingPress\bin on Windows.
npx -y @mvanhorn/printing-press-library install bento --cli-onlyThen install the focused Hermes skill.
From the Hermes CLI:
hermes skills install mvanhorn/printing-press-library/cli-skills/pp-bento --forceInside a Hermes chat session:
/skills install mvanhorn/printing-press-library/cli-skills/pp-bento --forceRestart the Hermes session or gateway if the newly installed skill is not visible immediately.
Install both the CLI binary and the focused OpenClaw skill. The installer defaults binaries to a per-user bin directory ($HOME/.local/bin on macOS/Linux, %LOCALAPPDATA%\Programs\PrintingPress\bin on Windows):
npx -y @mvanhorn/printing-press-library install bento --agent openclawRestart the OpenClaw session or gateway if the newly installed skill is not visible immediately.
This CLI ships an MCPB bundle — Claude Desktop's standard format for one-click MCP extension installs (no JSON config required).
To install:
- Download the
.mcpbfor your platform from the latest release. - Double-click the
.mcpbfile. Claude Desktop opens and walks you through the install. - Fill in
BENTO_PUBLISHABLE_KEYwhen Claude Desktop prompts you.
Requires Claude Desktop 1.0.0 or later. Pre-built bundles ship for macOS Apple Silicon (darwin-arm64) and Windows (amd64, arm64); for other platforms, use the manual config below.
Manual JSON config (advanced)
If you can't use the MCPB bundle (older Claude Desktop, unsupported platform), install the MCP binary and configure it manually.
Install the MCP binary from this CLI's published public-library entry or pre-built release.
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"bento": {
"command": "bento-pp-mcp",
"env": {
"BENTO_PUBLISHABLE_KEY": "<your-key>"
}
}
}
}Bento uses HTTP Basic with publishable_key:secret_key, plus a site_uuid attached as ?site_uuid= on GETs and as a body field on POSTs. Export the three env vars below in your shell (or your .envrc/.env file) — every command reads them automatically:
export BENTO_PUBLISHABLE_KEY=pk_...
export BENTO_SECRET_KEY=sk_...
export BENTO_SITE_UUID=...Get the keys from the Bento dashboard under Settings -> API Keys, and the site UUID under Account -> Site Settings. Verify with bento doctor. The CLI sets a descriptive User-Agent automatically — generic UAs are 403'd by Bento's Cloudflare edge.
# Verify auth, base URL, User-Agent, and live API reachability.
bento doctor
# Snapshot Bento into the local SQLite store. Required before any novel command.
bento sync --resources subscribers,tags,fields,broadcasts
# Local FTS5 search across notes/fields with structured filters.
bento subscribers find "vip" --tagged customer --json --select email,tags
# Run the full validation pipeline before any cold import.
bento hygiene scrub --in leads.csv --out-clean clean.csv --out-rejected rejected.csv
# Map a Vendure order export to Bento $purchase events; preview before sending.
bento events purchase-replay --from vendure-orders.json --dry-run --jsonThese capabilities aren't available in any other tool for this API.
-
sync diff— Show exactly which subscribers, tags, fields, or templates changed between two local snapshots since Bento has no since filter.Use this when you need to detect drift between Bento and your local source of truth, or audit what changed since the last sync.
bento sync diff --since 2026-05-19 --resource subscribers --json
-
tags drift— Surface tags whose subscriber counts swung more than N% week-over-week, catching misfiring workflows.Run weekly to catch silently-broken automations adding or removing tags unexpectedly.
bento tags drift --window 30d --threshold 25 --json
-
hygiene scrub— Run validation + jesses_ruleset + blacklist + content_moderation in one pass and emit a clean CSV plus a rejected CSV with reasons.Reach for this before importing any list you didn't generate yourself. Bad list = ISP penalty.
bento hygiene scrub --in leads.csv --out-clean clean.csv --out-rejected rejected.csv
-
events purchase-replay— Dry-run a Vendure order export against the Bento $purchase mapping and show the exact batch payload before sending.Use before any Vendure -> Bento purchase sync to catch shape mismatches without polluting subscriber history.
bento events purchase-replay --from vendure-orders.json --dry-run --json
-
events review-window— Emit a dated Bento event stream for orders shipped N days ago, skipping any order your existing third-party review platform already triggered.Use when running a Bento-side review trigger alongside a third-party review platform without double-mailing.
bento events review-window --shipped-csv orders.csv --delay 10d --skip-stamped --dry-run
-
fields lint— Diff local custom fields against a Vendure customer-schema file and flag missing, renamed, or type-mismatched fields.Use after any Vendure schema change to verify Bento custom fields still align.
bento fields lint --against vendure-schema.json --json
-
events lint— Warn when a payload uses /fetch/commands subscribe but the intent (welcome flow, tag-triggered automation) requires /batch/events to fire automations.Run this on any payload bound for /fetch/commands or /batch/events to catch silent automation misfires.
bento events lint --file events.jsonl --json
-
subscribers pre-delete— Show tags, events, revenue, and active automations a subscriber set carries before you file a GDPR data_deletion_request.Always run before any GDPR/CCPA batch deletion to confirm the right cohort and surface revenue impact.
bento subscribers pre-delete --emails-from gdpr-batch.txt --json
-
subscribers churn-risk— Rank subscribers by days-since-last-open/click against your cohort baseline before the next broadcast goes out.Use before queuing a broadcast to identify the cohort most likely to mark as spam.
bento subscribers churn-risk --threshold high --limit 100 --json
-
subscribers winback— Build a CSV of customers who bought once, lapsed 180+ days, and never opened the last 3 broadcasts.Reach for this when scoping a reactivation campaign with a custom decay window.
bento subscribers winback --lapsed 180d --last-purchased 90d --csv
-
broadcasts whatif— Estimate audience size, predicted opens, and hygiene-risk count for a draft broadcast before you queue it in the dashboard.Use before any broadcast send to sanity-check audience size and deliverability risk.
bento broadcasts whatif --segment seg_abc --json
-
subscribers find— Full-text search subscriber notes/fields plus structured filters in one query, exportable as CSV.Reach for this when building any ad-hoc cohort that isn't worth a segment in the dashboard.
bento subscribers find "first order" --tagged customer --purchased-after 365d --csv
Run bento-pp-cli --help for the full command reference and flag list.
Run bento-pp-cli <command> --help for full flag and argument details on any command below.
| Command | Purpose |
|---|---|
bento subscribers find <query> |
Local FTS5 search across subscriber notes/fields with structured filters (--tagged, --purchased-after, --csv) |
bento subscribers churn-risk |
Rank subscribers by days-since-last-engagement against your cohort baseline |
bento subscribers winback |
Build a CSV of one-time buyers who lapsed N days and stopped opening |
bento subscribers pre-delete |
Preview tags, events, revenue, and active automations before a GDPR delete |
bento subscribers fetch-batch |
Pull subscribers by email and populate the local store (non-Enterprise path) |
bento subscribers import-csv |
Import a Bento dashboard CSV export into the local subscriber store |
bento tags drift |
Tags whose subscriber counts swung more than N% in the window |
| Command | Purpose |
|---|---|
bento sync --resources <list> |
Snapshot Bento into local SQLite (subscribers, tags, fields, broadcasts, sequences, workflows, templates, segments) |
bento sync diff --since <ts> |
Diff two local snapshots — Bento has no since filter, so this is the only way to detect drift |
bento search <query> |
FTS5 across all synced resources (live API fallback when available) |
bento analytics --type <resource> --group-by <field> |
Count and group-by over synced data |
bento tail [resource] --interval 10s |
Poll the API and stream changes as NDJSON to stdout |
| Command | Purpose |
|---|---|
bento events purchase-replay --from vendure-orders.json |
Map a Vendure order export to Bento $purchase events; preview before sending |
bento events review-window --shipped-csv orders.csv --delay 10d |
Emit $review_request events for orders shipped N days ago, with --skip-stamped deduping |
bento events lint --file events.jsonl |
Catch payloads that use /fetch/commands subscribe when the intent needs /batch/events to fire automations |
| Command | Purpose |
|---|---|
bento hygiene scrub --in leads.csv --out-clean clean.csv --out-rejected rejected.csv |
Chain validation + jesses_ruleset + blacklist + content_moderation in one pass |
bento experimental create-validation |
Email validity heuristics for a single address |
bento experimental list <domain-or-ip> |
Check blacklists (domain or IP, not both) |
bento experimental create |
Content moderation score for a short string |
bento experimental create-gender |
Guess gender from first + last name (US Census data) |
bento experimental list-geolocation |
Geolocate an IP address |
| Command | Purpose |
|---|---|
bento broadcasts whatif --segment <id> |
Estimate audience size, predicted opens, and hygiene-risk count for a draft |
bento fields lint --against vendure-schema.json |
Diff local custom fields against a Vendure schema and flag missing, renamed, or type-mismatched fields |
| Command | Purpose |
|---|---|
bento fetch list |
List broadcasts |
bento fetch list-subscribers |
Fetch one or more subscribers by email |
bento fetch list-fields / list-tags |
List custom fields / tags |
bento fetch list-search |
Enterprise-only advanced subscriber search |
bento fetch create-subscribers / create-fields / create-tags |
Create individual resources |
bento fetch create |
Execute subscriber commands: add_tag, remove_tag, add_field, remove_field, subscribe, unsubscribe, change_email, add_tag_via_event |
bento batch create |
Up to 100 broadcasts per request |
bento batch create-emails |
Up to 60 priority-queued transactional emails per request |
bento batch create-events |
Up to 1,000 events per request (auto-creates users) |
bento batch create-subscribers |
Bulk subscriber upserts via Bento's import queues (no Flow side effects) |
| Command | Purpose |
|---|---|
bento stats list-site |
User / subscriber / unsubscriber counts for the site |
bento stats list |
Same counts for a specific segment (server-side only — never client-side) |
| Command | Purpose |
|---|---|
bento auth setup / set-token / status / logout |
Manage credentials |
bento doctor |
Verify auth, base URL, User-Agent, and live API reachability |
bento workflow archive / status |
One-shot sync of every resource for offline use; show archive state |
bento profile save / list / use / show / delete |
Save and reuse named flag sets |
bento import <resource> --input data.jsonl |
Import data from a JSONL file by issuing per-record POSTs |
| Command | Purpose |
|---|---|
bento which "<capability>" |
Natural-language lookup of the command that implements a capability |
bento agent-context |
Machine-readable JSON describing commands, flags, and auth (for runtime agent introspection) |
bento feedback "<note>" |
Capture friction locally at ~/.bento-pp-cli/feedback.jsonl (opt-in upstream POST via --send) |
bento version |
Print version |
bento completion <shell> |
Generate shell completion scripts |
# Human-readable table (default in terminal, JSON when piped)
bento-pp-cli experimental list
# JSON for scripting and agents
bento-pp-cli experimental list --json
# Filter to specific fields
bento-pp-cli experimental list --json --select id,name,status
# Dry run — show the request without sending
bento-pp-cli experimental list --dry-run
# Agent mode — JSON + compact + no prompts in one flag
bento-pp-cli experimental list --agentThis CLI is designed for AI agent consumption:
- Non-interactive - never prompts, every input is a flag
- Pipeable -
--jsonoutput to stdout, errors to stderr - Filterable -
--select id,namereturns only fields you need - Previewable -
--dry-runshows the request without sending - Explicit retries - add
--idempotentto create retries when a no-op success is acceptable - Confirmable -
--yesfor explicit confirmation of destructive actions - Piped input - write commands can accept structured input when their help lists
--stdin - Offline-friendly - sync/search commands can use the local SQLite store when available
- Agent-safe by default - no colors or formatting unless
--human-friendlyis set
Exit codes: 0 success, 2 usage error, 3 not found, 4 auth error, 5 API error, 7 rate limited, 10 config error.
Verified-flag recipes that combine the CLI's offline store, agent-mode, and Vendure bridge with shell tools an operator already uses.
# Pull every domain table into the local SQLite store, then sanity-check the archive.
bento sync --resources subscribers,tags,fields,broadcasts,sequences,workflows,templates,segments
bento workflow status --json | jq '{ts, resources: [.resources[] | {name, count, last_synced}]}'# Catch tags whose subscriber counts shifted >25% week-over-week (silently-broken automations).
bento tags drift --window 7d --threshold 25 --json | jq '.tags[] | select(.delta_pct | fabs > 50)'
# Audience size + hygiene-risk preview for the broadcast you're about to queue.
bento broadcasts whatif --segment vip-loyalty --json --select audience_size,predicted_opens,hygiene_risk_count# 1. Hygiene scrub. Clean rows go forward; rejected get logged.
bento hygiene scrub --in raw-leads.csv --out-clean clean.csv --out-rejected rejected.csv --rate 1.5
# 2. Convert the clean CSV to JSONL (one subscriber object per line).
tail -n +2 clean.csv | awk -F, '{printf "{\"email\":\"%s\"}\n", $1}' > clean.jsonl
# 3. Stream into Bento via the per-record import path.
bento import subscribers --input clean.jsonl# Inspect the mapping shape and dedupe keys without sending anything.
bento events purchase-replay --from vendure-orders.json --json \
--select 'events[].email,events[].details.unique.key'
# Once happy, flip the switch.
bento events purchase-replay --from vendure-orders.json --send# Daily cron: emit $review_request only for orders shipped 10 days ago today;
# --skip-stamped honors the local dedupe table so your existing review platform's mail never overlaps.
bento events review-window --shipped-csv orders.csv --delay 10d --skip-stamped --send# One-time buyers who lapsed 180d and stopped opening — straight to CSV for ad platforms.
bento subscribers winback --lapsed 180d --last-purchased 90d --csv > winback.csv
wc -l winback.csv# Show tags, events, revenue, and active automations before queuing a data-deletion batch.
bento subscribers pre-delete --emails-from gdpr-batch.txt --json | jq '.subjects[] | {email, revenue, active_automations}'# Catch renamed or type-mismatched custom fields before the next purchase event misfires.
bento fields lint --against vendure-schema.json --json# Local FTS across notes and custom fields with structured filters.
bento subscribers find "lifetime warranty" --tagged customer --purchased-after 365d --csv > cohort.csv
# Run analytics over the local store without touching the API.
bento analytics --type subscribers --group-by source --limit 20 --json# Save the agent-mode flag set as a named profile (flags on the save invocation are captured).
bento profile save agent --json --compact --no-color --no-input
# Apply with --profile on any future call.
bento subscribers find "vip" --profile agent# Resolve natural-language intent to the right command.
bento which "find lapsed customers"
bento which "import a CSV"
# Emit machine-readable command map for runtime introspection.
bento agent-context --prettybento-pp-cli doctorVerifies configuration, credentials, and connectivity to the API.
Config file: ~/.config/bento-pp-cli/config.toml
Static request headers can be configured under headers; per-command header overrides take precedence.
Environment variables:
| Name | Kind | Required | Description |
|---|---|---|---|
BENTO_PUBLISHABLE_KEY |
per_call | Yes | Basic auth username. Starts with pk_. Available in your Bento dashboard under Settings -> API Keys. |
BENTO_SECRET_KEY |
per_call | Yes | Basic auth password. Starts with sk_. Available in your Bento dashboard under Settings -> API Keys. Never expose client-side. |
BENTO_SITE_UUID |
per_call | Yes | Bento site identifier. Attached to every request as ?site_uuid= on GETs and in the JSON body on POSTs. Available in your Bento dashboard under Account -> Site Settings. |
Authentication errors (exit code 4)
- Run
bento-pp-cli doctorto check credentials - Verify the environment variable is set:
echo $BENTO_PUBLISHABLE_KEYNot found errors (exit code 3) - Check the resource ID is correct
- Run the
listcommand to see available items
- 403 with Cloudflare HTML body — User-Agent header is missing or generic.
bento doctorverifies UA is set correctly; the CLI hard-sets it automatically. - 400 'site_uuid required' — Run
bento auth statusto confirm BENTO_SITE_UUID is set. The CLI attaches site_uuid to every request automatically; this error means auth is misconfigured. - 401 'Author not authorized' — The from-email on
bento emails sendorbento broadcasts createis not a pre-authorized Author. Add the address in the Bento dashboard under Account -> Authors. - 429 rate-limited — Bento has no Retry-After header. The CLI implements client-side exponential backoff automatically. For batch ops, lower --batch-size or add --pace 60/min.
- subscriber import returns null then is missing — Imports are async (1-5 min). Use
bento subscribers get <email> --wait 5mto poll, orbento sync --resources subscribersand re-search locally.
This CLI is an independent reimplementation. Compared to Bento's own @bentonow/bento-cli and bento-mcp, it adds:
- Transactional email send (Ruby/PHP SDK only — not in the official CLI or MCP)
- Batch commands API (
/fetch/commandswithsubscribe/unsubscribe/add_tag) — only in the Python SDK upstream - Segments list/get endpoints — docs-only upstream
- Every experimental endpoint (validation, jesses_ruleset, blacklist, content_moderation, gender, geolocation)
- Data deletion requests endpoint (GDPR/CCPA)
- A local SQLite store with FTS5 search across subscribers/tags/fields/broadcasts
- 12 novel commands (snapshot diff, hygiene pipeline, purchase replay, churn-risk scoring, broadcast what-if, etc.) that have no upstream equivalent
- Single Go binary — no Node runtime required
- MCP server auto-derived from the Cobra command tree (every CLI command is reachable to agents)
These are limits of the Bento API itself, not of this CLI. None of them can be solved by an API wrapper:
- No broadcast send/schedule/stop endpoint. Broadcasts are dashboard-only — you can list and read them, but cannot trigger them via API.
- No workflow create/edit/pause endpoint. Same — dashboard-only.
- No sequence email reorder/delete endpoint. Edit individual emails inside a sequence in the dashboard.
- No standalone email-template listing. Templates are only accessible nested under broadcasts and sequences.
- No event history endpoint. Bento has
POST /batch/eventsto write but no list endpoint to read event history back. - Bulk subscriber listing requires the Enterprise tier. Mitigated locally via
subscribers fetch-batch(per-email pull) andsubscribers import-csv(load a dashboard CSV export into the local store). - No subscriber DELETE. Use the GDPR
/data_deletion_requestsendpoint to anonymize. - No documented webhook signature verification. Bento does not publish a signing scheme for inbound webhooks.
This CLI was built by studying these projects and resources:
- bento-cli — TypeScript
- bento-mcp — TypeScript
- bento-node-sdk — TypeScript
- bento-ruby-sdk — Ruby
- bento-python-sdk — Python
- bento-php-sdk — PHP
- n8n-nodes-bento — TypeScript
Generated by CLI Printing Press