Reach Ward
Reach Ward inventories local agent and MCP configurations, credential fingerprints, and visible or manually declared scopes. It reports risky settings and changes against a baseline. It is an advisory auditor, not an access-control boundary.
Built and maintained by Dragon Lady — GitHub.
Posture
- Linux first, Python 3.10 or newer. Apache-2.0.
- Offline by default. No telemetry, server execution, config edits, token rotation, browser-cookie access, or SQLite database access.
- No secret values, full arguments, file contents, or conversation history saved.
Credentials are represented by
fp:and 16 hexadecimal characters from HMAC-SHA256 with a private, random, per-installation 32-byte key. - Local reports and state contain source paths, server and variable names, command basenames, URL hosts, credential fingerprints, scopes, file metadata, and the configured GitHub username. Treat these reports as private metadata.
- Only Reach Ward's state and explicitly requested reports are written. Targets are opened read-only; original bytes, modes, size, and mtime are preserved.
- State directory: 0700. New state/report files: 0600 from creation. Unsafe existing state modes are refused, never repaired with chmod. Report output is create-only and never overwrites an existing file.
- No claim that a host, credential, package, server, or account is safe. A credential's existence does not prove it is valid or accessible to an agent.
Install
For the published release, install into an isolated environment:
python3 -m venv ~/.local/share/reachward-venv
~/.local/share/reachward-venv/bin/python -m pip install reachward==0.1.0
~/.local/share/reachward-venv/bin/reachward --help
Or use pipx install reachward==0.1.0. Check the
PyPI release page for availability.
Installation does not create a baseline, enable alerts, or install a timer.
To install a local source checkout:
python3 -m venv .venv
.venv/bin/python -m pip install .
.venv/bin/reachward --help
.venv/bin/reachward list-sources
.venv/bin/reachward scan
Runtime uses the standard library, plus tomli on Python 3.10 only. Building
requires setuptools; developing/tests require pytest. No package installs occur
during a scan.
Commands
reachward scan --format json
reachward scan --roots ~/projects/example
reachward baseline # after locally reviewing the inventory
reachward diff # exit 0 if unchanged with no coverage gaps
reachward scan --diff --format html --output reachward-report.html
reachward known add example-server
reachward known list
reachward known remove example-server
reachward list-sources
reachward report --since 7d
reachward report --send # aggregate alert; stdout unless configured
reachward print-timer --interval 1h # prints only; does not install or enable
Every command supports --format text|json|md|html and --output FILE. HTML is
self-contained and escaped; no scripts, remote assets, or executable links.
An existing baseline requires baseline --force to replace it. Establishing a
baseline does not approve findings or add names to the known-server list.
Exit codes: 0 no findings (or no changes for diff) and no coverage warnings;
1 findings, changes, or coverage/comparison warnings; 2 usage, fatal scan
errors, unsafe state/config modes, or runtime/delivery error. Credential-file
permission findings are advisories, not runtime errors.
Coverage is explicit in JSON and every report format. coverage is complete,
partial, or error; complete is false for partial/error coverage.
baseline_eligible distinguishes a partial inventory that can be recorded from
a fatal error that must not replace a baseline or be compared. Skipped source
symlinks and missing MCP-referenced env files produce coverage_warnings and
exit 1, without blocking the rest of the inventory. Parse failures, unreadable
files, and missing explicitly configured extra_sources/env_files remain
errors. Explicit env_files also remain required when they are symlinks.
Encrypted rclone sources retain their explicit uninspected status.
Baselines save per-source coverage and keyed source identities. Entries skipped
on either side are excluded from comparisons and listed in
comparison_warnings; recovering coverage alone is not an added server. A
skipped MCP config may conceal env-file references, so env sources not enumerated
on that side are conservatively excluded too. Inspect recovered sources and
refresh the baseline to establish comparison coverage. Encrypted sources cannot
be compared and remain a comparison warning. Missing optional default files are
normal: removing a previously read optional source still produces removals.
Metadata mtime alone is not an inventory change, preventing unrelated file edits
from flagging every server. Credential, mode, command, argument, destination and
other inventory changes are compared. Derived known and command_missing
fields, source identity metadata added by the auditor, and optional online-only
GitHub scopes/visibility are excluded. Local/manual scope changes are compared.
Baselines and fingerprints are local to
their key: do not compare baselines from different machines or regenerate a key
in place. Preserve fp.key with backups of the state.
Sources
| Collector | Sources |
|---|---|
| Cursor | ~/.cursor/mcp.json, root .cursor/mcp.json |
| Codex | ~/.codex/config.toml, ~/.codex/auth.json |
| Claude | ~/.claude.json including project maps, root .mcp.json, XDG Claude/claude_desktop_config.json |
| VS Code | XDG Code/User/mcp.json, root .vscode/mcp.json; JSONC accepted |
| Other | ~/.codeium/windsurf/mcp_config.json, ~/.gemini/settings.json, configured extra paths |
| GitHub CLI | XDG gh/hosts.yml, or GH_CONFIG_DIR/hosts.yml |
| rclone | XDG rclone/rclone.conf, or RCLONE_CONFIG |
| Environment | Explicit env_files, MCP envFile/env_file/plural fields and --env-file arguments |
| Manual | XDG reachward/cloud_connectors.toml |
XDG means $XDG_CONFIG_HOME, default ~/.config. Roots are explicit, not
recursively searched. Extra sources must be JSON/JSONC or TOML MCP maps. Relative
env-file references resolve against the MCP config directory; unresolved variable
references are not expanded. General shell syntax and multiline dotenv values
are not interpreted. Encrypted rclone configs never prompt for a password.
Whole configuration documents are parsed in memory; only the inventory metadata
described above is retained. No conversation history is collected.
All sources must be under $HOME. If HOME itself is an alias, its root is
canonicalized once, with an explicit coverage warning. Matching HOME-prefixed
XDG/config/source paths are normalized to that root. Source symlinks beneath the
root (including parent components) are skipped without opening their targets,
whether the target is inside or outside home. Reach Ward's own config and state
retain strict no-follow checks and are refused when unsafe. Special files,
files over 5 MiB, unreadable files and files changing during a read are not
inspected. Checks use no-follow directory/file descriptors.
hosts.yml is parsed using a restricted, non-executing subset covering gh's
host/user records; unsupported YAML is an explicit incomplete scan. Absence of
an inline token means keyring or absent: not inspected. Reach Ward never
queries the keyring, uses gh auth token, or claims a token is present there.
Findings
| Rule | Meaning |
|---|---|
RW-INLINE-SECRET |
Credential inline in an MCP config, header, URL auth/query, or recognized argument |
RW-FILE-MODE |
Credential file permits access beyond 0600, or immediate directory beyond 0700 |
RW-BROAD-SCOPE |
A listed GitHub/Google full-access scope, rclone Drive drive, or informational OneDrive drive type |
RW-STALE |
Old file metadata, rclone expiry over 30 days past, overdue manual review, or missing launch executable |
RW-UNKNOWN-SERVER |
Server name not in configured or locally managed known names |
RW-SERVER-CHANGED |
Known server's command, arguments, host or transport changed against baseline |
RW-UNPINNED-LAUNCH |
npx -y, uvx or pipx run without an exact package version; informational |
RW-DUP-CREDENTIAL |
Same fingerprint in more than one inventory entry; informational |
RW-KEY-FILE-REFERENCE |
Path-valued credential reference; target not inspected; informational |
Credential names use whole semantic words, including camelCase and plural
KEYS; compact GITHUBPAT/GITLABPAT are recognized explicitly. Ordinary
PATH, KEYBOARD, MAX_TOKENS, and TOKENIZERS_PARALLELISM settings do not
become credentials. Token count/limit/budget names are treated as settings;
recognized provider-shaped token values still take precedence under any name.
Path-valued GOOGLE_APPLICATION_CREDENTIALS, privateKeyPath, credentialsPath
and other credential names with a file/path word produce only reference field
names and keyed path fingerprints (key_file_references). They do not produce
inline-secret findings or feed path values into global credential redaction.
Targets are never opened or checked for existence/permissions. Reference
classification requires a path prefix (/, ~/, ./, ../) or a familiar
key-file suffix such as .json/.pem/.key; ambiguous opaque values retain
credential handling. Provider-shaped tokens override this classification even
inside a path. Arbitrary credential formats remain heuristic, not exhaustive.
Arguments and full commands are compared with keyed HMACs, not stored. The
argument shape uses only generic categories. Pin detection is conservative and
does not resolve packages or wrapper scripts. Scopes inferred from local files
or manual entries are declarations, not proof of actual provider permissions.
File age is not token age. Missing command checks use a fixed, configurable
command_search_path, defaulting to ~/.local/bin, /usr/local/bin, /usr/bin,
and /bin; the ambient shell PATH is ignored. A command containing a slash is
checked directly (relative to its config directory when needed). Nothing is
executed by this check. Client launch environments may still differ.
Configuration and alerts
Config: ~/.config/reachward/config.toml (0600), or global --config FILE.
See example config and
manual connectors. known add/remove updates
only Reach Ward's private known.json; it never edits the config. Configured
known names must be removed by explicitly editing the config.
command_search_path is an ordered array of absolute or ~/ directories;
empty/relative entries, colons and control characters are rejected. The same
search path is used by the optional online check and printed as an explicit
Environment="PATH=..." in print-timer. A custom --config path is retained in
the printed unit. Unit paths are quoted/escaped; recognizable credential-shaped
or control-containing paths are refused without echoing them. Printing units
loads configuration but creates no state and installs or starts nothing.
alert_channels accepts stdout, slack, ntfy, email; [] means stdout.
Configuring external channels explicitly enables alert networking. scan sends
new high findings and requested baseline changes; report --send sends all
current findings/changes. Messages contain only counts, rule IDs and severity,
ending with run reachward report locally for details (command in backticks).
No paths, hosts, server names, account identifiers, or fingerprints leave the
machine in alerts. Dedupe lasts 24 hours per rule/entry/channel/destination;
failed deliveries are retried on the next run. Each network operation uses a
5-second timeout; redirects are disabled. HTTPS is required, except on loopback;
SMTP requires TLS off loopback. Environment variable names hold references
to Slack webhook, ntfy token and SMTP password. Never inline a Slack webhook.
The optional scan --online runs exactly gh api -i /user and reads only
X-OAuth-Scopes. It does not run during normal scans. This uses the active gh CLI
identity, which may be affected by environment overrides; it does not prove
scopes for every saved credential. A missing header means unknown, not no scopes.
The gh executable is resolved from command_search_path.
No Google/Slack token introspection, MCP connections or other audit networking.
State under $XDG_STATE_HOME/reachward (default ~/.local/state/reachward):
fp.key, baseline.json, last.json, known.json, aggregate history.json
(30 days, at most 1,000 runs), and HMAC-only alerts.json dedupe receipts.
State is bounded to 5 MiB per file. A malicious process already running as the
same user can tamper with configs or local baselines; this tool does not defend
against a compromised user account. Run one writer per state directory.
Review and tests
For an executable before-and-after demonstration, see Reproduce a permission finding. It uses an isolated temporary home and fake credential, checks the installed package, and verifies that a separate operator permission correction clears the finding.
python3 -m pip install pytest
python3 -m pytest -q
Tests use a fake HOME and fake XDG directories. Coverage includes every rule, canaries in every command/format, target preservation, private files from creation, no-network/no-subprocess offline checks, diff mutations, parse errors, symlinks, FIFOs, bounded reads, and mocked online/alert paths. No GitHub Actions workflow is enabled. Timers are templates only; independently review a baseline, choose an executable path and approve scheduling before installing units.
Collector format references: VS Code MCP, Claude MCP, gh api, and rclone.
Metadata
Release files for reachward 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| reachward-0.1.0.tar.gz | 50.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| reachward-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 82.7 kB
Release files / reachward-0.1.0.tar.gz
| Download URL | reachward-0.1.0.tar.gz |
|---|---|
| Size | 50.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5b16e704c0d32a7549d7d64205dca38dacf37dd9d0da01fe500cec8586bb1760
|
|
BLAKE2b-256 checksum How to use checksums |
31c985a4a3bf8cf09f8e4c2e2c0d15acb3b9bd672ec1aad2300d1cf7a9f2aade
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / reachward-0.1.0-py3-none-any.whl
| Download URL | reachward-0.1.0-py3-none-any.whl |
|---|---|
| Size | 31.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d4cf3558bed4def821a8a6570ccaf465d6ab9d768c47b8aa4230c5abebc57592
|
|
BLAKE2b-256 checksum How to use checksums |
70b1dc42bea8f5644513a5fa58fdaf64f4999aabb91e825543270432244c9c2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log