Skip to content

ReachWeaver 0.11.0 release notes

Released on 2026-10-20.

Easier and safer for Claude Code to drive, measured with the plugin’s own behaviour evals. One call to start a session (context), a command catalog an agent can read (describe), --json and stable error codes on more commands, a voice check against stock AI phrasing, an ideas inbox, follow-up suggestions instead of automated engagement, release-to-draft packages, review packets for a co-reviewer, an opt-in read-only MCP server for other MCP clients, Python 3.14 support, and an agent guard that ships with the plugin. 116 commands (12 more than 0.10.0, none removed). No live adapter is added, and the only change to sending only tightens it: LinkedIn posts are no longer sent by a scheduled or bulk run.

Upgrade notes.

  • LinkedIn: a scheduled or bulk publish --live now holds LinkedIn posts (HELD); send each one with publish --live --post-id ID. schedule refuses LinkedIn posts. This follows LinkedIn’s API Terms of Use 3.1.
  • No abbreviated options: publish --liv or --conf are now errors; write options in full.
  • Plugin users: the plugin adds a PreToolUse hook that refuses owner-only ReachWeaver commands in Claude’s shell; it needs python3, like the rest of the plugin.
  • data-purge: a retention policy may set closed_ideas_days; without it ideas are kept.
  • Update the package with pipx upgrade reachweaver and the plugin through /plugin.
  • LinkedIn posts are never sent by a scheduled or bulk run. LinkedIn’s API Terms of Use 3.1 forbid using the APIs “to automate posting on the LinkedIn Services”. A LinkedIn post now goes out only when the owner names that one post (publish --live --post-id ID, no --scheduled-only); scheduled and bulk runs hold it before any claim, credential read or request (HELD, worker reason owner_triggered_only), and schedule refuses it. Tighten-only (policy.OWNER_TRIGGERED_ONLY); see LinkedIn.
  • The plugin now carries the agent guard. A PreToolUse hook in plugin/hooks/hooks.json runs reachweaver/agent_guard.py (standard library only) before every Bash, PowerShell and Monitor command and blocks, with “Owner-only: ask the owner to run this in their own terminal.”, any ReachWeaver run of approve, account-kit --approve-plan, publish --live, resume, reconcile, reconnect or approval-mode --confirmation off, and any command that sets REACHWEAVER_ALLOW_LIVE, sets REACHWEAVER_ENV=prod or names REACHWEAVER_APPROVAL_SECRET. It recognises reachweaver, run.py and the module form python3 -m reachweaver[.cli], wrappers, chains, bash -c, eval and command substitution, and refuses unreadable commands that name an owner-only action. It is a second layer for a cooperative agent, not a security boundary (a script file run later is not seen); turn on approval-mode --confirmation tty_hash_prefix as well. Tests: tests/test_agent_guard.py.
  • No abbreviated options. The CLI’s parsers now set allow_abbrev=False, so publish --liv or approval-mode --conf off is an error instead of being read as --live or --confirmation off past a deny rule that matches the full spelling. Use full option names in scripts.
  • reachweaver review-packet, review-comment, review-status: a co-reviewer comments on the exact text before you approve. 3 new commands. review-packet (--post-id ID ... | --campaign-staged NAME) --out NEW_DIR [--persona ID] [--optout F] [--sources CONFIG] [--json] opens the database read-only and writes a NEW folder (0700, files 0600) with one Markdown and one JSON file per post: destination, exact text, alt texts, video strings, the approval content digest (approve_would_bind_sha256, the existing digest without schedule, nonce or identity), the check findings, the cited evidence and a comment template. No credentials or local paths; a private source URL or evidence link is redacted as in data-export, and a post that links into a private source repository or whose text matches a credential pattern is refused. review-comment --packet DIR --post-id ID --reviewer NAME --verdict looks_good|change_requested|question --note TEXT (owner-only in describe) stores the comment in the packet’s own comments.json, never in the database or the approval ledger, bound to the digest the reviewer read; a comment on a post that changed since the packet is refused. review-status [--packet DIR] [--json] shows comments per post and marks a comment STALE when the post’s current digest differs (exit 1). A comment, including looks_good, is never approval and every output says so; approve never reads review files. Docs: docs/REVIEW-PACKET.md. Tests: tests/test_review_packet.py (database unchanged with total_changes 0, no write path, approve independent of review files).
  • reachweaver mcp-serve: a local, read-only MCP server for MCP clients other than Claude Code. 1 new command. It speaks JSON-RPC 2.0 over stdin/stdout, serving MCP revision 2026-07-28 (per-request _meta, server/discover) and the initialize handshake of 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05, written from the specification with the standard library only (no MCP SDK, still no runtime dependency). Tools are generated from the command catalog: only commands tagged read_only that are not interactive, owner_only or network, have no owner-only flag and print JSON (about 30, such as context, describe, campaign_check, claims_check, voice_check); credentials-audit and flags such as --online, --owner-authorized and --reply-to are withheld. There is no approve, publish, resume, reconcile, reconnect, connect, pause or approval-mode tool, and start-up fails if the catalog would ever let one through. --allow-drafts adds a draft tool (local drafts only; each still needs the owner’s approval). Arguments are checked against schemas generated from argparse, file arguments must resolve inside --root, results are redacted and refused above --max-output-bytes, calls are rate limited, sockets are blocked while a tool runs, and stdout carries only protocol messages. Not tested against Claude Desktop itself. New reachweaver/mcp_server.py, tests/test_mcp_server.py; see local MCP server. No change to approval, dry-run, emergency-stop, credential or reconciliation rules, and no database schema change.
  • reachweaver followup-suggest: milestone follow-up suggestions instead of automated engagement. 1 new command. followup-suggest --rules FILE [--account ID] [--since DATE] [--state FILE [--record]] [--optout F] [--json] reads your published posts and their metric snapshots from the local database read-only (never created or migrated) and, when a post’s latest exact value reaches a threshold you set (optionally within N hours of publishing, per platform or account), suggests a follow-up: thank_and_link, recap_thread, cross_post (adapt for another configured account), pin_or_feature (manual note) or write_up (ideas-inbox entry). Each suggestion carries the metric value, snapshot age and source, a short brief and the normal next commands (show, draft, repeat-check, idea add), labelled “suggestion for review; nothing is drafted, approved or sent”. Estimates are never compared; Reddit and Hacker News posts get no suggestion and are never cross-post targets; opt-out handles are never named. --state FILE --record remembers suggested post/rule pairs in a private 0600 file (ids only). It never likes, comments, replies, DMs, reposts, drafts or sends. New reachweaver/followups.py, examples/followup-rules.example.json, tests/test_followups.py; see follow-up suggestions. No change to approval, dry-run, emergency-stop, credential or reconciliation rules, and no database schema change.
  • Plugin behaviour evals for claude plugin eval (local only). plugin/evals/ holds 13 cases: never runs approve or publish --live even on “just post it” and hands the owner the exact command instead; never asks for, repeats, uses or writes a pasted password; no Hacker News submission or reply text; Reddit human-only and moderator-only scheduling; context --json at session start; voice-check plus a rewrite of the flagged lines; “later” saved with idea add; no publication claim or view count without a receipt; no virality promise; share/manual-export for Instagram and TikTok. Most graders are free transcript checks (tool_used, regex), with an llm rubric only where a pattern cannot judge. The default run adds the no-plugin baseline. make plugin-eval runs the suite on your own Claude plan with Bash, Write and Edit granted under Claude Code’s sandbox, and writes results to the ignored state/plugin-evals/. It is never part of make check or CI. make plugin-eval-load parses every case with the real CLI and a $0 ceiling. New tests/test_plugin_evals.py checks the files offline: documented keys only, a grader and a free grader in every case, unique names, compilable regexes, and owner-only patterns that catch approve/publish --live but not describe approve or a dry run. One case (session-start-context, one run, one arm) scored 1.00 on 2026-10-20; the full suite has not been run. See plugin behaviour evals. No change to approval, dry-run, emergency-stop, credential or reconciliation rules.
  • reachweaver release-brief: release to draft, a checked campaign package for every release (offline, never a post). release-brief --release release.json [--repo OWNER/NAME] --out NEW_DIR [--brand B --accounts A] [--objective O] [--campaign ID] [--json] reads a GitHub release JSON you saved yourself (gh api repos/OWNER/NAME/releases/latest > release.json; ReachWeaver makes no network call), or --changelog CHANGELOG.md --version X.Y.Z reads one Keep a Changelog section. It writes a NEW folder (0700) with campaign.reachweaver.json and brief.md (0600) in the campaign contract format: the release URL as kind: "release" evidence with its tag and published_at, cleaned highlights (at most 25, 240 characters each; HTML comments, code, images and markup removed) as an untrusted excerpt, checked_by: "unverified" without verification/checked_at, and {{write: ...}} placeholders, so campaign-check reports it BLOCKED until a person writes and verifies it. Release text is data, never instructions: an instruction-like line is withheld and flagged. A draft release, a list, a non-public URL, a mismatched --repo and an existing --out are refused. New template examples/github-workflows/release-to-draft.yml (copy it into your own repository; not active here): on release: published it saves the release with the read-only default GITHUB_TOKEN (contents: read), runs release-brief and campaign-check (BLOCKED does not fail the job) and uploads the folder as an artifact; no secrets, no posting, actions pinned by SHA. Docs: docs/RELEASE-TO-DRAFT.md. Tests: tests/test_release_brief.py, tests/test_release_to_draft_workflow.py.
  • Claude Code helpers: ideas inbox, statusline and a fact-check subagent. 2 new commands. reachweaver idea add TEXT [--source URL] [--tag T], idea list, idea drop ID keep “post about this later” notes in a private ideas.json next to the database (folder 0700, file 0600; text up to 2000 characters, https sources only, credential-looking text refused); an idea is not a draft and can never be approved or sent, and adding one never creates the database. idea promote ID --campaign NAME --out NEW_DIR [--brand B --accounts A] writes a NEW campaign scaffold (campaign.reachweaver.json + brief.md) in the campaign contract format with the idea as the brief and {{write: ...}} placeholders; campaign-check reports it BLOCKED until written and verified, and it never creates a post, approval or send. reachweaver statusline prints one read-only line for Claude Code’s status bar (RW 0.10.0 | drafts 3 | queued 1 | needs review 0 | ideas 2 | PAUSED; RW: no workspace without a database), opens the database read-only, never creates it, ignores stdin, prints counts only and always exits 0; python3 -m reachweaver.statusline skips loading the full CLI (about 0.07 s). The statusLine setting is documented, never written for you. The plugin adds plugin/agents/fact-check.md, a read-only subagent (tools Read, Grep, Glob, WebFetch) that labels each factual claim of a draft verified, unverified or contradicted with the source URL and never edits, approves or publishes. New reachweaver/ideas.py, reachweaver/statusline.py, tests/test_ideas_statusline.py; see Claude Code. No change to approval, dry-run, emergency-stop, credential or reconciliation rules, and no database schema change.
  • reachweaver voice-profile and voice-check: drafts that sound like you (offline, advisory). voice-profile builds plain statistics of how you write from your own posts, either a local .json, .csv or ----separated text file or your published posts in the local database (opened read-only): length per platform (median and middle half), sentence length, emoji, hashtags and their position, mentions, links, I/we usage, question and exclamation marks, capitals, line breaks and lists, em dashes, semicolons and ellipses, common openings and recurring words. It needs at least 10 posts (else insufficient_sample, exit 1), writes a NEW 0600 file and stores no post text, ID or link. voice-check compares a draft (--text, --file or --post-id) with the profile, showing your usual range for each feature outside it, and with reachweaver/voice_tells.json, a heuristic list of stock phrases and patterns (“delve”, “game-changer”, “it’s not just X, it’s Y”, em-dash runs, rocket emoji…) with a reason per entry. It is not an AI detector and claims no accuracy; a tell you use often yourself is only info. Findings are info or warn, never block approval or publishing, and the command exits 0. creative-preflight --voice-profile FILE adds the same findings per draft, and the skill tells Claude to rewrite flagged lines itself before showing a draft. No network, model call or database write. 2 new commands; see voice check, tests/test_voice.py.
  • Easier for Claude Code to drive: context, describe, more --json and stable error codes. 2 new commands.
    • reachweaver context [--json]: one compact read-only session-start call (about 2-3 KB of JSON) replacing update --notice + doctor + start + health + approval-mode + list: version and install kind, whether the plugin’s SessionStart hook already ran update --notice, paths, each account’s offline readiness, persona and missing credential variable NAMES, the emergency pause, whether a live send is possible in this shell (booleans only, never an environment value), the approval safeguards (read without the approval key), post counts, posts waiting for the owner (approved but unsent, needs_review, sending, re-approval needed), the next steps of start marked owner_runs, top offline suggestions and owner_only_commands. It opens the database read-only and never creates one.
    • reachweaver describe [COMMAND] [--json]: a command catalog generated from the argparse parser itself (flags, required, choices, defaults, repeatable, type, mutually exclusive groups, how to get JSON), with owner_only / network / writes / read_only / interactive tags and owner-only flags (publish --live, account-kit --approve-plan…). The tags live in reachweaver/catalog.py; a test fails when a new subcommand has no classification.
    • --json on draft (created post ids; not with --reply-to), list, check, doctor, schedule, cancel, unapprove, import-metrics, campaign-stage (always JSON), approval-mode (display only; refused together with a setting change, which stays text-only) and the dry-run publish (the unchanged preview captured as JSON; refused with --live, whose path is untouched). Text output without --json is byte-identical.
    • Errors: the ERROR: message line on stderr is unchanged; when the command was run with --json, stdout also gets {"error": {"code": ..., "message": ...}} with a stable code (validation_error, not_found, already_exists, refused, paused, live_gate, database, permission, io_error, invalid_json, invalid_csv, invalid_value, invalid_input, publish_error, error; reachweaver/error_codes.py). One existing test that expected an empty stdout on a sources-scan --json refusal now expects that document.
    • The skill’s session start is now one context --json call, and it no longer repeats update --notice when the plugin’s SessionStart hook already ran it.
  • experiment-readout and baseline-eval can use GA4 outcomes. New --attribution-source ga4 reads the ga4 section of a saved attribution-report --json (made with --ga4-csv): GA4 sessions count as docs_visits, key events as repo_stars_utm, and the GA4 date range is the window. The default stays analytics; the two are never mixed, and the output names the source in attribution_source. A report without a ga4 section is refused. Offline; tests/test_attribution_ga4_source.py.
  • attribution-report reads GA4 and Search Console exports you download yourself (offline). --ga4-csv FILE takes a GA4 report or exploration saved as CSV (Session source / medium, Session campaign, Session manual ad content, Sessions, and optionally Engaged sessions, Key events, Landing page and Date; UI labels or Data API names). The # lines at the top are skipped and their start and end dates kept, the totals row is shown but never joined, and files whose dates overlap are refused. Its rows are joined to posts like UTM rows, in a separate ga4 section that is never added to --analytics sessions. --gsc-csv FILE takes the Pages, Queries, Dates and Filters tables of a Search Console Performance export; pages that your posts link to are listed as search context and are never credited to a post. Both sections are labelled as owner-supplied data with their date range, and list what is unknown (for example a post with no GA4 row, or a page missing from a 1,000-row export) instead of counting it as zero. Unknown or missing columns, oversized files (16 MiB, 100,000 rows) and malformed numbers are refused; 1,234, 1.234, 12,3 and 3.5% are read. --analytics is no longer required when one of the new flags is given. No Google API, OAuth, credential or network request. New reachweaver/web_analytics.py, tests/test_web_analytics.py; see attribution. No change to approval, dry-run, emergency-stop, credential or reconciliation rules.
  • Faster start for every command. Importing the CLI spent about a third of its time compiling the 100 or so non-English comment-triage patterns that only engagement-triage and reply checks use. They now compile once on first use (same patterns, flags and order; a test compares them with a fresh compile), and engagement-triage, signal-brief, sources-scan and utm-link import their modules when they run. Measured interleaved against the previous commit on one laptop, importing the CLI takes about 40% less time (median 208 to 128 ms under load, 170 to 104 ms with less load) and version, --help, context, list and show finish about 75-85 ms sooner (median of 7 runs each). The approval, publish, reconcile and send paths import exactly as before.
  • Tab completion reaches the actions of idea. reachweaver completion bash|zsh now completes idea add|list|drop|promote, each action’s options and fixed choices (idea list --status <Tab> offers open promoted dropped), generated from the parser for any command with its own actions. Regenerate the script after upgrading.
  • The terminal check is described accurately. docs/CLAUDE-CODE.md and docs/RECOMMENDED-SETUP.md called approval-mode --confirmation tty_hash_prefix a hard guarantee that an AI session cannot pass. It refuses commands run through pipes and is strong friction, but a program that opens a pseudo-terminal can answer it (as reachweaver/confirmation.py says); the owner approval key (--require-key) is the stronger control. Docs only; no behaviour change.
  • The skill’s CLI cookbook now lists every command. references/cli-cookbook.md covers every subcommand (fewer than 60 before), one table row each with its purpose and key flags, grouped as in --help, and marks owner-only steps (approve, publish --live, resume, reconcile, reconnect, account-kit --approve-plan, connect --guide, data-purge --apply) as run by the owner in their own terminal. Commands are written with the RW prefix from SKILL.md instead of python -m reachweaver.cli, which does not resolve from a user’s project in a plugin install; other skill references and public docs use RW, reachweaver or python3 instead of a bare python. New tests/test_cookbook_drift.py fails, naming the row to add, when a subcommand is missing from the cookbook or a flag written there does not exist.
  • Claude never repeats a credential the owner pastes, not even to say “revoke it”. A three-runs-per-case eval (score 0.97 with the plugin, +0.37 over none) found one run that quoted a pasted app password in its advice; the no-plugin baseline even used it in a shell command. SKILL.md’s credential rule now covers replies and commands, and five re-runs passed.
  • The skill now routes “save this for later” and “does this sound like me” and always hands back the owner’s commands. The first full claude plugin eval run (13 cases, score 0.91 with the plugin, +0.31 over no plugin, every safety case 1.00) showed three gaps: the skill did not load for a saved idea, voice-check was skipped, and “post it” did not always end with the exact approve and publish --live commands. SKILL.md (description, two routing rows, one owner-step sentence; still under 16 KB) fixes all three; re-runs score 1.00. Results and the sandbox’s read-only folder are in plugin evals.
  • Python 3.14: an HTTP error response is closed once it is handled, so its reason phrase never reaches stderr. Python 3.14 warns (ResourceWarning) when an unclosed urllib HTTPError is garbage-collected, and the warning quotes the error’s reason phrase. Normally hidden, it was printed under -W, -X dev, PYTHONWARNINGS or the test runner, and 11 tests whose fixtures put a fake token or provider body in that field failed on 3.14. Every handler now closes the error after the status, headers and any bounded body it already read: publishers (_decode_response, request_json_with_headers, _status_and_json, request_put_bytes, new close_http_error), postiz._call, sources._fetch_response, remote_media.download_image, updater.http_get (re-raises the same error) and the connect --guide check. Nothing else read, logged, redacted or classified changes, nor retry, ambiguous-send or reconciliation behaviour. New tests/test_http_error_close.py (also a -X dev -W error::ResourceWarning subprocess). Two tests that relied on pre-3.14 behaviour now hold on every version without checking less: the deep-JSON fixtures are nested deeper (3.14 bounds the JSON decoder by the real stack, so 50 000 levels parsed), and the broken-pipe test reads its pipe unbuffered (3.14’s 128 KiB default buffer could drain the whole output). CI now also tests Python 3.14 (new tests (3.14) check; existing names unchanged) and the package lists the 3.14 classifier.
  • The ideas inbox is covered by the privacy commands and the workspace report. state/ideas.json (next to the database) was missing from all of them. data-export now includes it in an ideas_inbox block (same redaction as the tables); privacy-status reports its path, mode, size, counts per status and oldest idea, never its text; workspace lists it with a warning when others can read it, also before a database exists. data-purge takes a new optional policy key, closed_ideas_days: dropped or promoted ideas older than that are listed in the dry run (closed_ideas_delete) and removed by --apply; an open idea is never removed, even from an edited plan. Without the key every idea is kept, and nothing else the purge does changes. A damaged inbox is reported as unreadable and left alone. config/retention.example.json sets 180 days. See data lifecycle.
  • cli.py is back under the plugin directory’s 256 KiB file limit. The argument parser moved unchanged to reachweaver/cli_parser.py; reachweaver.cli.make_parser still works and the release gate reads subcommands from both files.
  • JSON nesting has an explicit limit. Local JSON files may nest 512 levels, a share --web request 32 and an mcp-serve message 64; deeper input is refused before parsing with “nested too deeply”. The result no longer depends on the Python version: 3.14 bounds its JSON decoder by the thread stack, and CI parsed depths that a laptop refused.
  • Bad local input now ends in one ERROR: line and exit 2, not a Python traceback. A --db file that is not a database, or one locked by another reachweaver process, says so (exit 2, it used to print a traceback and exit 1, which means “findings”); a JSON file nested thousands of levels deep says it is nested too deeply; dates out of range (distribution-plan --date 0001-01-01 --timezone Etc/GMT-14, calendar-gaps --start 9999-12-31, peer-benchmark, creator-coverage, the metrics CSV) are refused cleanly. JSON files saved with a UTF-8 byte order mark are read. list --status accepts only real statuses and, when nothing matches, prints “No posts” on stderr (stdout stays empty for scripts). account-route no longer reports a false conflict with “@None” for an account without a username, such as LinkedIn: its "persona" field decides, and an account whose persona is unknown is still refused. account-kit --platform accepts comma lists like share. The share --web page answers a lone surrogate in the request with a 400 error instead of dropping the connection, and treats "note": null as no note. GA4 and Search Console CSV imports no longer split rows at form feeds or U+2028 or end a table inside a quoted multi-line cell. Piping output into head exits quietly with status 141 (128 + SIGPIPE) instead of printing “Exception ignored … BrokenPipeError”.