Security and trust policy
Reporting a vulnerability (private)
Section titled “Reporting a vulnerability (private)”Please do not open a public issue, discussion or pull request for a vulnerability, and never paste credentials, tokens, cookies, session files or exploit details publicly.
- Report privately by e-mail to mohsen.seyedkazemi@gmail.com with
ReachWeaver securityin the subject. Describe the version and a safe reproduction, without sharing live credentials; revoke exposed tokens at their provider first. - If GitHub private vulnerability reporting is enabled and visible, that private channel can be used instead. Do not assume the form is active solely because the link exists.
- If neither private channel is usable, submit a public issue saying only
requesting a private security contact, with no incident details. See root SECURITY.md.
In scope: credential or token exposure (in output, logs, audit rows, errors or redirects, or through $CREDENTIALS_DIRECTORY file handling), any bypass of the human-approval gate, the dry-run default, the live gate (--live + REACHWEAVER_ALLOW_LIVE=1 + REACHWEAVER_ENV=prod) or the emergency pause, two accounts reading the same credential despite the env_prefix overlap rule, automatic retry of an ambiguous send, unintended network access from offline commands or tests, and SSRF/redirect issues in platform clients. Include the affected version or commit, an offline reproduction where possible, and the impact. If you leaked a real credential while testing, revoke it at the provider first.
Operators and maintainers: the short, actionable security checklist covers the first live send, each release and incident response.
Supported versions: the latest main branch. Fixes land there; there are no long-term support branches yet.
Threat model
Section titled “Threat model”Assets: account authority, access tokens, authenticated sessions, unpublished research, marketing schedule, account reputation, analytics and private user interactions. Trust boundaries: AI model, local host, repository/CI, external webpages and social platform APIs.
The detailed STRIDE threat model covers token theft, prompt injection, wrong-account publishing, supply-chain integrity, redirects, registry poisoning, SQLite tampering, approval spoofing, replay and external backends. Internal working design-review details are maintained privately; the public policy below summarizes controls and remaining limitations.
Controls implemented
Section titled “Controls implemented”- No account secrets accepted in
config/accounts.json: field name guard blocks token/password/session/cookie keys. Config is in.gitignore. - No file-based implicit
.envloading; the human deployer controls the credential source. Credentials come from environment variables or, when$CREDENTIALS_DIRECTORYis set (systemdLoadCredential=, issue #5), only from files in that directory, through one accessor (accounts.credential). That accessor refuses a relative or group-/world-writable directory, symlinks (O_NOFOLLOW), FIFOs and other non-regular files, files over 64 KiB and non-UTF-8 content, never falls back to the environment while the directory is set, and names only the variable in errors. File values are redacted byprivacy.redactexactly like secret environment variables.tests/test_hardening.pychecks that no module inreachweaver/reads a credential any other way. - The default
publishcommand never contacts a platform. Explicit--live,REACHWEAVER_ALLOW_LIVE=1,REACHWEAVER_ENV=prod(issue #5, owner-approved 2026-10-09), approval and due timestamp are enforced. The three live opt-ins are checked bypolicy.require_live_gatebefore any account is loaded, any credential is read or any request is made, for native adapters and the Postiz backend. An unrecognizedREACHWEAVER_ENVvalue is never echoed. Approval is bound to the exact payload (issue #7, owner-approved 2026-10-08).approvestores a SHA-256 over the canonical JSON ofaccount_id,platform, byte-exactcontent,scheduled_at,source_urlandcampaign.claim_sendingre-verifies it under itsBEGIN IMMEDIATElock before any budget or network use, and both live senders (CLI loop and Postiz) check it before the identity preflight. A changed post, or one approved before the upgrade (no grandfathering), fails closed: it staysapproved/scheduled, nothing is claimed, andhealthlists it asattentionuntil a human runsunapprove,show,approve. Residual risk: the hash is unkeyed, so a process that can write the DB can rewrite the content and recompute the hash. It detects unsanctioned mutation; it does not authenticate the approver, andapproved_byis attribution only. A keyed HMAC or signed approval (decision D7) is still open. See threat model T7/T8 and SECURITY-REVIEW-PROPOSAL-7-11.md. - SQLite transactional claims prevent duplicate concurrent claims on the same state database.
- Unknown send outcomes are non-retriable until humans reconcile the remote record.
- Emergency pause prevents new claims. Audit log captures state transitions without API payloads or secrets.
- HTTPS-only Mastodon domain origin validation limits accidental local-network targets.
- External API errors are reduced to sanitized codes rather than printing provider responses or credential-bearing URLs.
- Tests mock API requests; CI performs no social publishing and receives no account credentials.
tests/test_no_network.pypatches every socket entry point and fails if importing the package or running the offline CLI paths attempts a connection or DNS lookup. - CI runs
scripts/secret_scan.pyover every tracked file (private key blocks, AWS/GitHub/Slack/Telegram tokens, JWTs, Bluesky app-password shapes outside example files, literal password/token assignments). It printspath:line:pattern-idonly, never the matched value. Synthetic test fixtures are suppressed only through reviewed, reason-bearing entries in.secret-scan-allow. It is pattern-based, so it is a backstop, not a guarantee. - The runtime is stdlib-only, so no third-party package runs with account tokens.
scripts/check_runtime_deps.py(CI hygiene job,make deps,tests/test_dependency_policy.py) fails ifpyproject.tomldeclares any runtime or optional dependency, statically or through[project] dynamic/[tool.setuptools.dynamic], and its message requires a pinnedpip-auditstep and a lock file first. Development tools (ruff, mypy) run throughpipx runat exact versions, outside the project environment. See ADR-0002. - CI runs with
permissions: contents: read, receives no social credentials, and pins every third-party GitHub Action to a full commit SHA. - Recorded provider responses must be scrubbed before commit: no tokens or cookies, and synthetic DIDs, handles and user ids. Provider fixtures may not use
.secret-scan-allowentries (tests/fixtures/providers/README.md). load_accountsrefuses identical or overlappingenv_prefixvalues (issue #5): no two accounts may have credential variable names, over every adapter suffix and the Postiz_POSTIZ_API_KEYsuffix, that are equal or where one is a string prefix of the other (accounts.env_prefix_conflicts). The error names the two account ids and variable names only.- The repository’s
.claude/settings.jsondenies Claude Code any Bash command that runsreachweaver ... approve,publish --live, or containsREACHWEAVER_ALLOW_LIVE=1orREACHWEAVER_ENV=prod. Since #7 (owner decision 7, 2026-10-09) it also deniesapproval-mode ... --confirmation off, any Bash command namingREACHWEAVER_APPROVAL_SECRET, and reading aLoadCredential=file of that name. It is a harness rule for this checkout, not a ReachWeaver control: it does not bind other agents, other checkouts or a human. - The Claude Code plugin carries a
PreToolUsehook (plugin/hooks/hooks.json, scriptreachweaver/agent_guard.py, owner-reviewed 2026-10-20) that blocks, with exit code 2, anyBash,PowerShellorMonitorcommand that runs ReachWeaver (reachweaver,run.py,python3 -m reachweaver[.cli]) withapprove,review(the guided approval loop, #11),account-kit --approve-plan,publish --live,resume,reconcile,reconnectorapproval-mode --confirmation off, or that setsREACHWEAVER_ALLOW_LIVE, setsREACHWEAVER_ENV=prodor namesREACHWEAVER_APPROVAL_SECRET. It parses chains, wrappers,bash -c,evaland command substitution and refuses unreadable or inline-Python commands that name an owner-only action. Like item 15 it is defence in depth for a cooperative agent, not a boundary: a script file run later, an encoded PowerShell command or another language’s subprocess call is not seen. The CLI refuses abbreviated long options (allow_abbrev=False), so a shortened--livcannot slip past either rule.
Approval expiry (issue #39, draft for owner review)
Section titled “Approval expiry (issue #39, draft for owner review)”approval-mode --max-age-hours N (1 to 8760) turns on an approval time-to-live. It is off by default, so nothing changes until the owner sets it. When on, the claim, schedule, calendar-plan --apply, health and show all treat a post whose approved_at is older than N hours as approval_expired_reapproval_required: the post stays approved or scheduled, nothing is sent, no budget is used, one audit event is written, and the fix is the usual unapprove, show, approve. Tampering keeps its own reason code (the expiry is checked last). schedule refuses a due time that falls after the approval would lapse. The setting is tighten-only: a command accepts only a lower value; raising or clearing it is an edit of the settings row by the owner. An unreadable stored value fails closed (every approval counts as expired). Limits: the clock is the host clock, and the age runs from approve (a schedule keeps approved_at), so a post approved far ahead of a due time must be approved again closer to it.
Not yet implemented
Section titled “Not yet implemented”- OS keyring/KMS integrations, OAuth PKCE, rotation automation, organization RBAC, a keyed or signed approval binding and a human-presence check on
approve(decisions D7 and D6), encrypted-at-rest DB, browser-mediated OAuth and immutable signed audit trails. - Secret scanning of git history, untracked files and encoded/obfuscated values (the CI scanner covers tracked files only).
- A dependency vulnerability audit (
pip-audit). It is not needed while the runtime has no third-party dependencies, which control 11 enforces. It must land in the same change as the first runtime dependency. - Tracing, platform incident alerts, per-provider spend budgets and dependency-compromise tests (remaining issue #26 scope). An offline
healthcommand, a read-onlydead-letterview, a local opt-out list and a tighten-only caps file now exist; see worker operations. Provider 429 handling (per-account cooldowns, a bounded read-only preflight retry,caps.jsonversion 2 provider budgets) exists for issue #11 and never resends a claimed post; see provider rate limits. - DNS rebinding protection for user-configured social server origins. Avoid untrusted configuration and only use reputable service origins; verify DNS and TLS on the trusted host.
Human account workflow
Section titled “Human account workflow”- Sign in yourself on the provider’s official website; complete MFA yourself. Generate least-privilege tokens with scoped publishing access where possible.
- Enter tokens only into local trusted secret manager/environment, never into public git, Claude prompts, screenshots, logs or documentation.
- Review account and organization permissions before posting. Pause publishing when a token leaks, account identity changes, or a misleading scheduled claim is detected.
- Respect provider’s platform rules and national laws (privacy, disclosures, advertising, copyright and consumer protection). Always mark paid/sponsored partnerships as required.
- For emergency stop:
reachweaver pause. If a post is insending/needs_review, check external result before manual reconciliation.
Security issues must be reported privately; see Reporting a vulnerability above.
Additional v0.2 controls and residual risk
Section titled “Additional v0.2 controls and residual risk”- Campaign and brand JSON reject secret-like key names (token/password/session/cookie/API key); source URLs must use HTTPS with a public hostname, no literal IP/localhost/credentials. No URL is fetched by the validator. This is format preflight rather than a complete SSRF defense for future fetchers; network fetch implementations will need DNS/IP/redirect defenses.
- Brand account allowlists and configured account/platform checking are enforced before campaign staging. This prevents accidental cross-project staging, but approval and publication still depend on the operator correctly identifying and reviewing the destination.
- Staged campaigns are immutable by
(campaign_id,variant_id)fingerprint; there is no silent overwrite or automatic retry. Idempotency is local to one SQLite DB. Campaign evidence is archived as owner-declared metadata, not independent proof that a URL supports a claim. - External webpage/repository/social copy can contain adversarial instructions. Treat as untrusted data; do not execute or install third-party skill scripts automatically. Review SHA/permission/license/trust before installing external skills.
- The experiment evaluator requires native snapshots at matched ages and never calls remote APIs, but self-reported plan timestamps and analytics can still be forged. Do not call the output causal or proof of algorithm weights.
- Local campaign/brand/evidence files can contain sensitive product plans even without tokens. Store real profiles under ignored
data/brands/(or outside the repo) and keep exports private. Sample files only contain safe public examples.
v0.3 identity and local state controls
Section titled “v0.3 identity and local state controls”- Added explicit
connect-check --onlinefor identity verification against the intended Bluesky handle, Mastodon local account, or Telegram bot and channel identity. TelegramgetChatMemberchecks channel posting permission. This is an authenticated external check, not social publishing. It cannot prove Mastodonwrite:statusesor Bluesky future posting eligibility. No OAuth redirect flow is implemented. - Live sends now verify intended remote identity before claiming an approved post. Mismatch/read failures block without sending and preserve original approval. After a claim, delivery errors remain in
needs_reviewand are never retried automatically. - A strict no-redirect HTTP opener prevents forwarding Bearer Authorization headers or Telegram bot-token URL paths to a provider-supplied redirect destination. Response bodies remain unlogged and HTTP/network errors have generic codes. Dynamic Mastodon server configuration still requires a trusted operator; DNS rebinding and transport-level supply-chain risk are not completely mitigated.
- POSIX SQLite
state/must have mode 0700 and the database 0600. New files are created privately; existing group/world-readable directories or files and DB symlinks are rejected. This prevents accidental local disclosure, not malicious access by the same Unix user or root. Windows ACL hardening remains future work. showdisplays exact private draft copy for human approval. Do not run it in public CI logs. Usepublish --post-idfor one-post supervised tests, afterdoctor/connect-checkand the actual content approval. The ordinary multi-post publisher remains supported only for trusted supervised/approved workflows.
See FIRST-LIVE-PILOT.md for owner-assisted onboarding and evidence collection without sharing credentials.
Privacy lifecycle controls (issue #26 slice)
Section titled “Privacy lifecycle controls (issue #26 slice)”reachweaver.privacy.redact()/structured_log()strip bearer/basic credentials, JWTs, Telegram bot tokens (also in bot URLs), Bluesky app-password shapes, GitHubghp_/github_pat_tokens, URL passwords, secret-namedkey=valuepairs and, verbatim, values ofREACHWEAVER_*_TOKEN/_PASSWORD/_APP_PASSWORD/_BOT_TOKEN(and_SECRET/_API_KEY) env vars. Pattern redaction is defense in depth, not a guarantee: an unrecognized secret format can still pass, so keep secrets out of post text and error strings in the first place.data-exportredacts secret-looking text, but the export still contains private drafts and analytics. It is created as a new 0600 file and never overwrites an existing path.data-purgeis dry-run unless--apply. Apply re-checks hard rules inside oneBEGIN IMMEDIATEtransaction: active/in-flight posts (draft…needs_review) are never touched,send_claimedevents younger than 48 h and events ofsending/needs_reviewposts are kept,campaign_variantsrows/fingerprints are never deleted, and published posts keepremote_id/remote_url/status/timestamps (content becomes[purged]). Only counts are logged (privacy_purge).secure_deleteis enabled for the purge, but backups, filesystem snapshots, prior exports and the remote platforms retain their copies.privacy-statusprints only the names of secret env vars present, never their values.
See privacy lifecycle.
Credential hygiene audit (issue #5 slice)
Section titled “Credential hygiene audit (issue #5 slice)”credentials-audit [--history FILE] [--json]is offline. It reads credentials through the same accessor as the adapters (the environment, or the files in$CREDENTIALS_DIRECTORYwhen that is set) plus the accounts config, and reports variable names only:credential_source, set and missing per account,placeholder_value,orphan_credential,credential_value_reused(compared through salted in-memory SHA-256 digests that are never output),shared_credential_variableandoverlapping_env_prefix(for raw account lists;load_accountsrefuses both),unsafe_credential_file,unsafe_credentials_directory,env_credential_shadowed(an exported credential ignored because a credentials directory is set) and, for history files named explicitly,shell_history_literalwith line numbers only andhistory_file_not_private. It exits 1 on findings. Missing credentials are status, not findings.- It opens no database and uses no network.
doctorpoints to it. tests/test_credential_hygiene.pyalso asserts, with sockets blocked, thatpublish --livewith missing, placeholder or provider-rejected (HTTP 401) credentials sends nothing, records no send claim, leaves the approval untouched and never prints the value.- Provisioning patterns (hidden
read,pass,op run,secret-tool, systemdLoadCredential=and theEnvironmentFile0600 fallback) and per-provider revocation are in ACCOUNT-ONBOARDING.md.
