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 --livenow holds LinkedIn posts (HELD); send each one withpublish --live --post-id ID.schedulerefuses LinkedIn posts. This follows LinkedIn’s API Terms of Use 3.1. - No abbreviated options:
publish --livor--confare now errors; write options in full. - Plugin users: the plugin adds a
PreToolUsehook that refuses owner-only ReachWeaver commands in Claude’s shell; it needspython3, like the rest of the plugin. data-purge: a retention policy may setclosed_ideas_days; without it ideas are kept.- Update the package with
pipx upgrade reachweaverand the plugin through/plugin.
Security
Section titled “Security”- 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 reasonowner_triggered_only), andschedulerefuses it. Tighten-only (policy.OWNER_TRIGGERED_ONLY); see LinkedIn. - The plugin now carries the agent guard. A
PreToolUsehook inplugin/hooks/hooks.jsonrunsreachweaver/agent_guard.py(standard library only) before everyBash,PowerShellandMonitorcommand and blocks, with “Owner-only: ask the owner to run this in their own terminal.”, any ReachWeaver run ofapprove,account-kit --approve-plan,publish --live,resume,reconcile,reconnectorapproval-mode --confirmation off, and any command that setsREACHWEAVER_ALLOW_LIVE, setsREACHWEAVER_ENV=prodor namesREACHWEAVER_APPROVAL_SECRET. It recognisesreachweaver,run.pyand the module formpython3 -m reachweaver[.cli], wrappers, chains,bash -c,evaland 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 onapproval-mode --confirmation tty_hash_prefixas well. Tests:tests/test_agent_guard.py. - No abbreviated options. The CLI’s parsers now set
allow_abbrev=False, sopublish --livorapproval-mode --conf offis an error instead of being read as--liveor--confirmation offpast 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), thecheckfindings, the cited evidence and a comment template. No credentials or local paths; a private source URL or evidence link is redacted as indata-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 indescribe) stores the comment in the packet’s owncomments.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, includinglooks_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 withtotal_changes0, 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 theinitializehandshake 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 taggedread_onlythat are notinteractive,owner_onlyornetwork, have no owner-only flag and print JSON (about 30, such ascontext,describe,campaign_check,claims_check,voice_check);credentials-auditand flags such as--online,--owner-authorizedand--reply-toare 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-draftsadds adrafttool (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. Newreachweaver/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) orwrite_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 --recordremembers suggested post/rule pairs in a private 0600 file (ids only). It never likes, comments, replies, DMs, reposts, drafts or sends. Newreachweaver/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 runsapproveorpublish --liveeven 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 --jsonat session start;voice-checkplus a rewrite of the flagged lines; “later” saved withidea add; no publication claim or view count without a receipt; no virality promise;share/manual-exportfor Instagram and TikTok. Most graders are free transcript checks (tool_used,regex), with anllmrubric only where a pattern cannot judge. The default run adds the no-plugin baseline.make plugin-evalruns the suite on your own Claude plan with Bash, Write and Edit granted under Claude Code’s sandbox, and writes results to the ignoredstate/plugin-evals/. It is never part ofmake checkor CI.make plugin-eval-loadparses every case with the real CLI and a $0 ceiling. Newtests/test_plugin_evals.pychecks the files offline: documented keys only, a grader and a free grader in every case, unique names, compilable regexes, and owner-only patterns that catchapprove/publish --livebut notdescribe approveor 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.Zreads one Keep a Changelog section. It writes a NEW folder (0700) withcampaign.reachweaver.jsonandbrief.md(0600) in the campaign contract format: the release URL askind: "release"evidence with its tag andpublished_at, cleaned highlights (at most 25, 240 characters each; HTML comments, code, images and markup removed) as an untrusted excerpt,checked_by: "unverified"withoutverification/checked_at, and{{write: ...}}placeholders, socampaign-checkreports 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--repoand an existing--outare refused. New templateexamples/github-workflows/release-to-draft.yml(copy it into your own repository; not active here): onrelease: publishedit saves the release with the read-only defaultGITHUB_TOKEN(contents: read), runsrelease-briefandcampaign-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,
statuslineand afact-checksubagent. 2 new commands.reachweaver idea add TEXT [--source URL] [--tag T],idea list,idea drop IDkeep “post about this later” notes in a privateideas.jsonnext 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-checkreports it BLOCKED until written and verified, and it never creates a post, approval or send.reachweaver statuslineprints 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 workspacewithout a database), opens the database read-only, never creates it, ignores stdin, prints counts only and always exits 0;python3 -m reachweaver.statuslineskips loading the full CLI (about 0.07 s). ThestatusLinesetting is documented, never written for you. The plugin addsplugin/agents/fact-check.md, a read-only subagent (toolsRead, Grep, Glob, WebFetch) that labels each factual claim of a draftverified,unverifiedorcontradictedwith the source URL and never edits, approves or publishes. Newreachweaver/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-profileandvoice-check: drafts that sound like you (offline, advisory).voice-profilebuilds plain statistics of how you write from your own posts, either a local.json,.csvor----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 (elseinsufficient_sample, exit 1), writes a NEW 0600 file and stores no post text, ID or link.voice-checkcompares a draft (--text,--fileor--post-id) with the profile, showing your usual range for each feature outside it, and withreachweaver/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 onlyinfo. Findings areinfoorwarn, never block approval or publishing, and the command exits 0.creative-preflight --voice-profile FILEadds 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--jsonand stable error codes. 2 new commands.reachweaver context [--json]: one compact read-only session-start call (about 2-3 KB of JSON) replacingupdate --notice+doctor+start+health+approval-mode+list: version and install kind, whether the plugin’s SessionStart hook already ranupdate --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 ofstartmarkedowner_runs, top offline suggestions andowner_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), withowner_only/network/writes/read_only/interactivetags and owner-only flags (publish --live,account-kit --approve-plan…). The tags live inreachweaver/catalog.py; a test fails when a new subcommand has no classification.--jsonondraft(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-runpublish(the unchanged preview captured as JSON; refused with--live, whose path is untouched). Text output without--jsonis byte-identical.- Errors: the
ERROR: messageline 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 asources-scan --jsonrefusal now expects that document. - The skill’s session start is now one
context --jsoncall, and it no longer repeatsupdate --noticewhen the plugin’s SessionStart hook already ran it.
experiment-readoutandbaseline-evalcan use GA4 outcomes. New--attribution-source ga4reads thega4section of a savedattribution-report --json(made with--ga4-csv): GA4 sessions count asdocs_visits, key events asrepo_stars_utm, and the GA4 date range is the window. The default staysanalytics; the two are never mixed, and the output names the source inattribution_source. A report without aga4section is refused. Offline;tests/test_attribution_ga4_source.py.attribution-reportreads GA4 and Search Console exports you download yourself (offline).--ga4-csv FILEtakes 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 separatega4section that is never added to--analyticssessions.--gsc-csv FILEtakes 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,3and3.5%are read.--analyticsis no longer required when one of the new flags is given. No Google API, OAuth, credential or network request. Newreachweaver/web_analytics.py,tests/test_web_analytics.py; see attribution. No change to approval, dry-run, emergency-stop, credential or reconciliation rules.
Changed
Section titled “Changed”- 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-triageand reply checks use. They now compile once on first use (same patterns, flags and order; a test compares them with a fresh compile), andengagement-triage,signal-brief,sources-scanandutm-linkimport 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) andversion,--help,context,listandshowfinish 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|zshnow completesidea add|list|drop|promote, each action’s options and fixed choices (idea list --status <Tab>offersopen 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.mdanddocs/RECOMMENDED-SETUP.mdcalledapproval-mode --confirmation tty_hash_prefixa 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 (asreachweaver/confirmation.pysays); 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.mdcovers 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 theRWprefix fromSKILL.mdinstead ofpython -m reachweaver.cli, which does not resolve from a user’s project in a plugin install; other skill references and public docs useRW,reachweaverorpython3instead of a barepython. Newtests/test_cookbook_drift.pyfails, 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 evalrun (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-checkwas skipped, and “post it” did not always end with the exactapproveandpublish --livecommands.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 unclosedurllibHTTPErroris garbage-collected, and the warning quotes the error’s reason phrase. Normally hidden, it was printed under-W,-X dev,PYTHONWARNINGSor 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, newclose_http_error),postiz._call,sources._fetch_response,remote_media.download_image,updater.http_get(re-raises the same error) and theconnect --guidecheck. Nothing else read, logged, redacted or classified changes, nor retry, ambiguous-send or reconciliation behaviour. Newtests/test_http_error_close.py(also a-X dev -W error::ResourceWarningsubprocess). 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 (newtests (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-exportnow includes it in anideas_inboxblock (same redaction as the tables);privacy-statusreports its path, mode, size, counts per status and oldest idea, never its text;workspacelists it with a warning when others can read it, also before a database exists.data-purgetakes 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 asunreadableand left alone.config/retention.example.jsonsets 180 days. See data lifecycle. cli.pyis back under the plugin directory’s 256 KiB file limit. The argument parser moved unchanged toreachweaver/cli_parser.py;reachweaver.cli.make_parserstill 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 --webrequest 32 and anmcp-servemessage 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--dbfile 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 --statusaccepts only real statuses and, when nothing matches, prints “No posts” on stderr (stdout stays empty for scripts).account-routeno 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 --platformaccepts comma lists likeshare. Theshare --webpage answers a lone surrogate in the request with a 400 error instead of dropping the connection, and treats"note": nullas 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 intoheadexits quietly with status 141 (128 + SIGPIPE) instead of printing “Exception ignored … BrokenPipeError”.
