Agent Reach is not a normal web server, so the main attack surface is not SQL injection or request floods against a public endpoint. The real surface is more dangerous for this product category: local host mutation, supply-chain installation, credential/cookie extraction, subprocess execution, browser profile access, agent-visible docs, and unbounded URL/media processing.
Production readiness: not recommended for broad agent/operator use until P0/P1 issues below are fixed.
Security posture: high-risk defaults, mostly from installer behavior and credential handling.
DX posture: docs/API mismatch is severe enough that LLMs and humans will call commands that do not exist.
UX posture: users are likely to be surprised by filesystem mutations, auto-installs, silent fallbacks, and stale command docs.
Performance posture: acceptable for ad hoc CLI usage, but slow/hang-prone for CI/MCP/bot loops.
Dynamic checks performed:
- Imported channel handlers and tested malicious host routing cases.
- Exercised
Config.to_dict() redaction behavior with cookie/session fields.
- Injected a fake
rookiepy module to validate the Xueqiu cookie loading path.
- Ran the test suite with time limits. The suite collected 162 tests and reached roughly 63% progress before timing out in the current environment, around
tests/test_core.py::TestAgentReach::test_doctor_report. This suggests the suite mixes unit tests with real doctor/probe behavior and lacks strict global test budgets/mocks.
Tooling limitations:
- Local
ruff, bandit, pip-audit, semgrep, and mypy were not installed in the container, so this report is primarily manual plus targeted dynamic repros.
- I did a dependency freshness/vulnerability spot check separately; see the dependency section.
Severity model
- P0 / Critical: likely host compromise, credential compromise, large trust violation, or default behavior that can break user machines.
- P1 / High: exploitable security gap, costly privacy/cost/resource surprise, major agent/product failure, or common path denial-of-service.
- P2 / Medium: important correctness, reliability, hardening, or maintainability issue.
- P3 / Low: cleanup, polish, weak signals, or future-proofing.
Findings
P0-01 — Installer performs high-risk host mutations and unpinned third-party installs by default
Owners: Security, DX, UX, Team Lead
Files: agent_reach/cli.py
Evidence:
install --safe exists, but safe mode is opt-in (cli.py:71-74).
_install_system_deps() modifies system package sources and installs packages (cli.py:510-635).
- It downloads GitHub CLI key material via
curl and writes to /usr/share/keyrings and /etc/apt/sources.list.d (cli.py:527-545).
- It downloads the NodeSource setup script and executes it with
bash (cli.py:573-583).
- It globally installs Node packages such as
undici (cli.py:607-609) and later optional channel tools via pipx, uv, or npm -g (cli.py:680-935).
- Several subprocess return codes are ignored, so partial failure can be reported as success or continue into later steps.
Impact:
A tool whose purpose is to empower agents is automatically modifying the host and adding package sources. That is exactly the path attackers want: compromise a package source, npm package, curl target, DNS, MITM on a misconfigured system, or a local shell environment, and the installer becomes a high-leverage execution chain. Even without compromise, it can break machines by changing Node/npm/apt state.
Tie-breaker verdict: this is the highest-risk issue in the repo. The project’s convenience layer is acting like a bootstrapper with root-level blast radius.
Recommended fix:
- Make safe mode the default. Rename current behavior to something explicit like
--unsafe-system-install or --yes-modify-system.
- Never execute remote setup scripts automatically. Print commands, or use signed repositories with documented verification.
- Pin external tools and package versions. Prefer hashes/lockfiles where possible.
- Check every subprocess return code with
check=True or explicit handling.
- Split installation into layers:
agent-reach install --python-only
agent-reach install --agents-skill
agent-reach install --external-tools --yes
agent-reach install --system --yes-i-understand
- Emit a machine-readable install plan before changes:
{
"will_modify_system": true,
"writes": ["/usr/share/keyrings/...", "/etc/apt/sources.list.d/..."],
"commands": ["apt-get update", "npm install -g ..."],
"requires_confirmation": true
}
P0-02 — Credential and cookie handling leaks secrets through CLI args, logs, docs, and incomplete masking
Owners: Security, UX, Edge Case Master
Files: agent_reach/config.py, agent_reach/cookie_extract.py, agent_reach/cli.py, docs
Evidence:
configure accepts secrets as positional CLI args (cli.py:80-90), which are commonly exposed in shell history, process listings, terminal recordings, LLM tool traces, and CI logs.
Config.to_dict() only masks keys containing key, token, password, or proxy (config.py:102-110). It does not mask xhs_cookie, twitter_ct0, bilibili_sessdata, or xueqiu_cookie.
- Browser extraction stores full cookies for XHS/Xueqiu/Bilibili/Twitter in config (
cookie_extract.py:243-289).
- Cookie extraction serializes whole cookie sets for some platforms, not just minimal needed fields (
cookie_extract.py:23-40, cookie_extract.py:137-143).
Dynamic repro:
Config.to_dict() masks twitter_auth_token, but leaks fields like:
{
"xhs_cookie": "a=secret; web_session=leak",
"twitter_ct0": "csrf-secret",
"bilibili_sessdata": "sess-secret",
"xueqiu_cookie": "xq_a_token=secret"
}
Impact:
These values are account credentials in practice. A leaked cookie or CSRF/session token can let another process or person act as the user, scrape authenticated content, or burn paid API quota. Because this is agent-facing software, “printed config” and “copied command” are not minor risks; they are normal operating paths.
Recommended fix:
- Add secret-safe configuration methods:
agent-reach configure groq-key --stdin
agent-reach configure twitter-cookies --from-browser chrome
agent-reach configure xhs-cookies --file ./cookies.json
- Use
getpass for interactive secrets.
- Deprecate raw positional secret values. Keep backward compatibility with a warning for one release.
- Redact keys containing any of:
SECRET_KEYWORDS = (
"key", "token", "password", "proxy", "cookie", "session", "sess",
"csrf", "ct0", "auth", "secret", "credential", "bearer"
)
- Never show full config by default. Provide
--show-secrets only with an explicit danger prompt.
- Store only minimum cookie fields required per backend, not whole cookies.
P1-03 — Domain routing accepts malicious lookalike hosts
Owners: Security, Edge Case Master, UX
Files: agent_reach/channels/*.py
Evidence:
Multiple channel handlers use substring checks against urlparse(url).netloc.lower().
Examples:
github.py:17
twitter.py:17
youtube.py:33
reddit.py:45
bilibili.py:44
linkedin.py:20
v2ex.py:33
xueqiu.py:158
xiaohongshu.py:158
Dynamic repro:
The following malicious or unrelated domains are currently accepted as trusted platform URLs:
https://github.com.evil.test/owner/repo -> GitHubChannel.can_handle(...) == True
https://notx.com/path -> TwitterChannel.can_handle(...) == True
https://youtube.com.evil.test/watch?v=x -> YouTubeChannel.can_handle(...) == True
https://reddit.com.evil.test/r/test -> RedditChannel.can_handle(...) == True
https://bilibili.com.evil.test/video -> BilibiliChannel.can_handle(...) == True
https://xiaohongshu.com.evil.test/explore -> XiaoHongShuChannel.can_handle(...) == True
https://linkedin.com.evil.test/in/me -> LinkedInChannel.can_handle(...) == True
https://v2ex.com.evil.test/t/1 -> V2EXChannel.can_handle(...) == True
https://xueqiu.com.evil.test/S/SH000001 -> XueqiuChannel.can_handle(...) == True
Impact:
A malicious URL can be routed into a platform-specific handler. Depending on the handler, this can cause wrong backend selection, credential/cookie use against attacker-controlled hosts, privacy leaks, or user/agent confusion. It is especially dangerous because the product is designed for agents that may classify and fetch URLs automatically.
Recommended fix:
Centralize URL host validation:
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"http", "https"}
def host_matches(url: str, *domains: str) -> bool:
parsed = urlparse(url)
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
return False
host = (parsed.hostname or "").rstrip(".").lower()
domains = tuple(d.lower().rstrip(".") for d in domains)
return any(host == d or host.endswith("." + d) for d in domains)
Then update handlers:
return host_matches(url, "github.com")
return host_matches(url, "x.com", "twitter.com")
return host_matches(url, "youtube.com", "youtu.be")
Add explicit tests for negative lookalike hosts.
P1-04 — Transcription path allows resource exhaustion, privacy/cost surprises, and leftover temporary data
Owners: Security, Performance, UX
Files: agent_reach/transcribe.py
Evidence:
yt-dlp is called on arbitrary URL input with no --no-playlist, no max file size, no max duration, and a 30-minute timeout (transcribe.py:77-98).
- Local files are accepted directly (
transcribe.py:230-234) with no size or duration limit.
- Temp directories are created with
tempfile.mkdtemp(prefix="transcribe-") and intentionally left behind (transcribe.py:214-228).
- Auto provider mode falls back from Groq to OpenAI per chunk on any
TranscribeError (transcribe.py:249-261).
Impact:
A user or agent can accidentally or maliciously transcribe an enormous playlist, a very long video, a local sensitive file, or many chunks. This can burn API quota, fill disk, consume CPU via ffmpeg, and leak media to a paid third-party provider the user did not expect. The temp-dir behavior leaves source/compressed/chunked audio on disk.
Recommended fix:
--max-download-mb 250
--max-duration-min 180
--no-playlist
--keep-temp
--allow-paid-fallback
- Use
TemporaryDirectory() by default and only preserve intermediates under --keep-temp.
- Add
yt-dlp flags:
--no-playlist
--playlist-items 1
--max-filesize <limit>
--socket-timeout 20
--retries 3
- Probe duration with
ffprobe before processing.
- Make provider fallback explicit. Do not silently send chunks to OpenAI after Groq fails unless the user opted into paid/provider fallback.
- Categorize errors:
- 401/403: disable provider for entire run.
- 429: bounded retry/backoff.
- 5xx/network: retry, then fallback only if explicitly allowed.
P1-05 — doctor has side effects and can become slow/hang-prone in tests, CI, and MCP contexts
Owners: Performance, UX, DX
Files: agent_reach/cli.py, agent_reach/doctor.py, agent_reach/backends/opencli.py, agent_reach/integrations/mcp_server.py
Evidence:
_cmd_doctor() calls _install_skill() after generating the doctor report (cli.py:1447-1464). A diagnostic command mutates the filesystem.
opencli_status() shells out to opencli --version and opencli daemon status and scans browser extension dirs (backends/opencli.py:63-136). Multiple channels can call this repeatedly.
- The MCP server’s async
get_status handler calls synchronous doctor_report() directly (integrations/mcp_server.py:44-53).
- Test suite execution timed out in this environment while running doctor-related tests.
Impact:
Diagnostics should be safe, fast, and side-effect-free. Here they install skills, touch multiple directories, shell out, and perform network/backend checks. In an agent loop or MCP server, this can turn a status call into a latency spike or filesystem mutation. In CI, it makes otherwise simple tests brittle and slow.
Recommended fix:
- Remove auto skill installation from
doctor.
- Add modes:
agent-reach doctor --fast # local config + binaries only
agent-reach doctor --network # external platform probes
agent-reach doctor --json
- Cache expensive checks per process/run:
@functools.lru_cache(maxsize=1)
def cached_opencli_status() -> OpenCLIStatus:
return opencli_status()
- In MCP, run doctor in a worker thread or provide a lightweight cached snapshot.
- Mark integration tests separately and mock all external probes by default.
P1-06 — scripts/transcribe_xiaoyuzhou.sh uses predictable tmp paths and unbounded network/media operations
Owners: Security, Performance, Edge Case Master
Files: scripts/transcribe_xiaoyuzhou.sh
Evidence:
- Predictable temp directory:
TMPDIR="/tmp/xiaoyuzhou_$$" (scripts/transcribe_xiaoyuzhou.sh:28).
- Page fetch uses
curl -s "$URL" with no timeout, no max size, and no --fail (scripts/transcribe_xiaoyuzhou.sh:55).
- Audio fetch uses
curl -sL -o with no timeout or size bound (scripts/transcribe_xiaoyuzhou.sh:70).
- Config path is interpolated into an embedded Python one-liner (
scripts/transcribe_xiaoyuzhou.sh:34).
- Uses
ffprobe, ffmpeg, and bc, but does not check all dependencies up front.
- Writes final output to a user-provided path/default under
/tmp (scripts/transcribe_xiaoyuzhou.sh:27, :262).
Impact:
A predictable temp path creates collision/symlink/race risk on multi-user machines. Unbounded network/media processing can hang, fill disk, or burn CPU. The script is also more fragile than the Python transcription module and should not be the hardened path.
Recommended fix:
TMPDIR="$(mktemp -d -t xiaoyuzhou.XXXXXX)"
trap 'rm -rf "$TMPDIR"' EXIT INT TERM
Use bounded curl:
curl --fail --show-error --location \
--connect-timeout 10 --max-time 60 \
--output "$file" "$URL"
Pass config path through environment:
CONFIG_FILE="$CONFIG_FILE" python3 - <<'PY'
import os, yaml
path = os.environ["CONFIG_FILE"]
...
PY
Prefer porting this script into the Python CLI and deprecating the shell script.
P1-07 — Docs and agent-facing API describe commands that do not exist
Owners: DX, UX, Team Lead
Files: llms.txt, test.sh, CLAUDE.md, README.md, docs/*, agent_reach/cli.py
Evidence:
- The CLI defines only:
setup, install, configure, doctor, uninstall, skill, format, transcribe, check-update, watch, version (cli.py:50-133).
llms.txt tells agents to use commands like agent-reach read <url> and agent-reach search-twitter "query" (llms.txt:13).
test.sh invokes nonexistent commands: agent-reach read, search, search-github, search-twitter, search-reddit, search-youtube, search-bilibili, search-xhs (test.sh:58-74).
CLAUDE.md says every channel implements read(url), search(query), and check(), but the base channel contract does not enforce this, and the actual CLI does not expose those commands.
Impact:
This is not cosmetic. This product is explicitly agent-facing. Stale llms.txt and test docs are an API bug because agents will read them and call invalid commands. Users will also conclude the install is broken when the actual code is merely narrower than the docs claim.
Recommended fix:
Choose one product boundary:
- Installer/capability-manager product: remove all
read/search claims from docs and tests. Publish exact backend command templates instead.
- Unified wrapper product: implement
agent-reach read, agent-reach search, and per-platform search commands with a stable channel interface.
Do not leave it halfway. Halfway is the worst option: it maximizes support burden and minimizes trust.
Recommended agent-facing endpoint:
agent-reach capabilities --json
Example output:
{
"version": "0.x.y",
"commands": ["doctor", "configure", "transcribe"],
"channels": {
"github": {"status": "available", "backend": "gh", "read_template": "gh api ..."},
"twitter": {"status": "missing", "install_hint": "agent-reach install --channels twitter"}
}
}
P2-08 — Xueqiu browser-cookie loading path is silently broken
Owners: Edge Case Master, Security, UX
Files: agent_reach/channels/xueqiu.py
Evidence:
_load_cookies_from_browser() builds a http.cookiejar.CookieJar, then calls self.session.cookies.set(...) (xueqiu.py:81-89). CookieJar does not expose the requests.cookies.RequestsCookieJar.set method.
Dynamic repro:
Injecting a fake rookiepy that returns an xq_a_token cookie causes _load_cookies_from_browser() to catch an exception and return False. The cookie jar remains empty. Because the function catches broad exceptions, users receive no actionable diagnosis.
Impact:
A documented authentication path fails silently. Users fall back to unauthenticated behavior, confusing rate limits, or manual cookie config. Security-wise, broad silent failure makes it harder to distinguish “no cookies found” from “cookie code is broken.”
Recommended fix:
Use requests.Session().cookies as a RequestsCookieJar, or construct proper Cookie objects for CookieJar. Prefer the simpler route:
self.session.cookies.set(
c["name"],
c["value"],
domain=c.get("domain") or ".xueqiu.com",
path=c.get("path") or "/",
)
Add a regression test with fake rookiepy.
P2-09 — Config writes are permission-conscious but not symlink-safe or atomic
Owners: Security, DX
Files: agent_reach/config.py, agent_reach/cookie_extract.py
Evidence:
- Config dir creation uses
mkdir(parents=True, exist_ok=True) without setting 0700 (config.py:37-39).
- Config file writes use
os.open(..., O_WRONLY | O_CREAT | O_TRUNC, 0o600) (config.py:54-62), but do not use O_NOFOLLOW where available and are not atomic.
- The fallback uses plain
open() (config.py:63-67).
- Cookie extractor uses a similar owner-only helper with a plain
open() fallback (cookie_extract.py:151-170).
Impact:
The file mode is good for the common case. But a local attacker or broken environment can exploit symlinks or partial writes. Atomicity matters because these files contain credentials and agents may read them while they are being written.
Recommended fix:
- Set config directory to
0700 on create and warn if broader.
- Write to a temp file in the same directory, fsync, chmod
0600, then os.replace().
- Use
O_NOFOLLOW where available.
- On Unix, lstat before write and reject symlinks.
P2-10 — Docker cookie update path has temp-file and container-selection edge cases
Owners: Security, Edge Case Master, UX
Files: agent_reach/cli.py
Evidence:
_configure_xhs_cookies() detects a Docker container by docker ps --filter name=xiaohongshu-mcp --format {{.Names}} and strips stdout (cli.py:1263-1270). If multiple containers match, the name can contain newlines.
- The temp cookie file is created with
NamedTemporaryFile(delete=False) and unlinked only after docker cp succeeds (cli.py:1291-1303). Exceptions before unlink leave cookie material in /tmp.
- The code restarts the selected container (
cli.py:1310-1320) without an exact-match prompt.
Impact:
This is mostly a local footgun, but it deals with live account cookies. Multi-container environments, failed docker cp, or weird container names can leave secrets on disk or restart the wrong service.
Recommended fix:
- Use
try/finally around temp unlink.
- Require exact container ID selection if more than one match exists.
- Prefer
docker cp - or bind-mounted config paths to avoid temp cookie files.
- Show a clear preflight summary before restarting containers.
P2-11 — Some HTTP reads and query parameters are unbounded or not encoded
Owners: Security, Performance, Edge Case Master
Files: agent_reach/channels/v2ex.py, agent_reach/channels/xueqiu.py, other channel modules
Evidence:
- V2EX URL construction inserts
node_name and username into paths without URL encoding (v2ex.py:89-93, v2ex.py:175-177).
- Xueqiu stock symbols and list limits are inserted without strong normalization/bounds (
xueqiu.py:196, xueqiu.py:218-230, xueqiu.py:290-303).
- Several channel methods read response bodies directly without explicit max response size.
Impact:
This is not catastrophic in a CLI, but it is agent-facing code. Agents will pass malformed, adversarial, or copy-pasted values. Unbounded response and parameter handling creates weird failures, slow runs, excessive output, and possible backend abuse.
Recommended fix:
- Encode all path components with
urllib.parse.quote.
- Clamp all
limit arguments with per-method caps.
- Enforce response byte limits and content-type sanity checks.
- Use a shared HTTP client wrapper with timeout, retry policy, max bytes, and user-agent.
P2-12 — Optional dependency constraints are not enforced by package installation
Owners: Security, DX
Files: pyproject.toml, constraints.txt
Evidence:
pyproject.toml uses broad dependency lower bounds such as yt-dlp>=2024.0 and requests>=2.28.
constraints.txt pins concrete versions, but normal package installation will not use it unless the user explicitly installs with -c constraints.txt.
- The constraints file pins
yt-dlp==2025.5.22, while the Python package allows much older or newer versions.
Impact:
The project has two dependency stories: broad runtime ranges and a constraint file most users will never apply. That makes local behavior hard to reproduce and weakens supply-chain control.
Recommended fix:
- Decide whether this is a library or an application.
- For a library: keep broad compatible ranges, but test a matrix and document minimums.
- For a CLI application: use a lockfile or pinned application bundle.
- Add CI dependency vulnerability scanning.
- Document installation commands that actually use constraints when intended:
pip install -c constraints.txt agent-reach
P3-13 — SECURITY.md excludes dependency vulnerabilities, but dependencies are the product’s highest-risk layer
Owners: Security, DX
Files: SECURITY.md
Evidence:
SECURITY.md says dependency vulnerabilities are out of scope unless they directly impact project code.
Impact:
For this repository, external tools and packages are not incidental. The product installs and orchestrates them. Dependency and supply-chain vulnerabilities should be first-class security reports.
Recommended fix:
Accept reports for dependencies when Agent Reach pins, recommends, installs, shells out to, or passes credentials through them.
P3-14 — Config.get() prioritizes config file over environment variables
Owners: UX, DX
Files: agent_reach/config.py
Evidence:
Config.get() checks self.data first, then environment variables (config.py:69-78).
Impact:
Many CLI users expect env vars to override config files for one-off runs and CI. Current behavior can surprise users who believe GROQ_API_KEY=... agent-reach transcribe ... overrides a stale file value.
Recommended fix:
Either reverse precedence or document it clearly and add AGENT_REACH_CONFIG_PRECEDENCE=file|env.
P3-15 — Version comparison is simplistic
Owners: DX
Files: agent_reach/cli.py
Evidence:
_is_newer_version() parses dotted digits only (cli.py:1658-1674).
Impact:
Pre-releases, post-releases, build metadata, and non-standard tags may compare incorrectly.
Recommended fix:
Use packaging.version.Version.
Expert pass summaries
Security expert
Primary concern: the project handles credentials and tells users/agents to install external tooling, but the trust boundaries are not explicit. The installer is too powerful by default, and the credential redaction model is incomplete.
Security priorities:
- Make host mutation opt-in and auditable.
- Stop secrets from entering CLI args and logs.
- Fix URL host validation.
- Add resource controls around all untrusted URL/media paths.
- Improve config write atomicity and symlink safety.
Exploit classes considered:
- Supply-chain compromise through remote setup scripts and global npm/pipx installs.
- Cookie exfiltration through logs/config dumps/agent traces.
- Lookalike-domain routing.
- Local temp-file collisions and secret residue.
- Agent-induced resource exhaustion through media URLs and diagnostics.
Performance expert
This code is probably fine for a human running one command occasionally. It is not fine for agents polling status, MCP clients calling status, or CI. The doctor path is too heavy, repeated OpenCLI checks should be cached, and transcribe has no practical resource budget.
Performance priorities:
- Make
doctor --fast local-only and cache expensive checks.
- Add concurrent bounded probes for network checks.
- Add max bytes/duration/chunks to transcription.
- Clamp API
limit parameters and response body sizes.
- Remove repeated filesystem mutations from status paths.
UX expert
The worst UX issue is not visual. It is broken expectation management. The docs say commands exist that do not exist. doctor mutates the filesystem. install performs system changes unless the user knows to pass --safe. transcribe --provider auto can switch providers silently.
UX priorities:
- Make dangerous actions explicit and reversible.
- Make docs truthful before adding features.
- Make errors actionable, especially for cookie extraction.
- Never silently change provider/cost/privacy domain.
- Validate unknown channel names instead of silently ignoring them.
DX expert
The project is currently LLM-hostile despite being agent-oriented. llms.txt is stale, CLAUDE.md describes contracts that are not enforced, and cli.py is doing too much in one 1,800-line module.
DX priorities:
- Define the real public API: installer/capability manager vs unified read/search wrapper.
- Create a single source of truth for commands and channel capabilities.
- Split
cli.py into install/config/doctor/skill/transcribe modules.
- Add typed channel interfaces and contract tests.
- Separate unit tests from integration tests.
Edge Case Master
The interesting bugs are not exotic; they are “obvious after seeing them”:
"x.com" in "notx.com" routes notx.com as Twitter/X.
github.com.evil.test routes as GitHub.
Config.to_dict() masks token, but not cookie, sessdata, or ct0.
- Xueqiu’s rookiepy path calls
.set() on a CookieJar and then swallows the exception.
- Docker container name discovery can return multiple names and restart the wrong one.
- A diagnostic command installs skills.
- A shell script uses
/tmp/xiaoyuzhou_$$ for temp data.
Edge-case priorities:
- Build adversarial tests for all platform router hostnames.
- Add fixture-based cookie extraction tests.
- Test multiple matching Docker containers.
- Test no-network/no-binary environments.
- Test agent-facing docs by running every command shown in
llms.txt and test.sh.
Dependency freshness notes
This is not a full dependency audit, but these checks matter:
constraints.txt pins yt-dlp==2025.5.22. A later NVD entry reports a Windows command-injection vulnerability affecting yt-dlp versions 2025.06.25 and below when --exec is used, fixed in 2025.07.21. Agent Reach does not appear to use --exec, so this is not directly exploitable from current transcribe.py, but the pinned version is stale and within the affected range.
- PyPI currently lists a newer
yt-dlp release than the pinned constraint.
- The project’s broad dependency ranges mean users may install versions very different from the tested constraint set.
Recommended action: add automated dependency scanning and decide whether constraints are authoritative.
Immediate patch plan
First 24 hours
- Fix host validation centrally and add negative tests for lookalike hosts.
- Expand secret redaction to include cookies/session/auth/csrf fields.
- Remove skill installation side effects from
doctor.
- Update
llms.txt, test.sh, and docs to remove nonexistent commands or clearly mark them as planned.
- Fix Xueqiu rookiepy cookie loading.
- Change shell script temp dir to
mktemp -d and add bounded curl flags.
- Validate
--channels; fail or warn on unknown names.
- Add pytest timeout and mark integration tests separately.
First week
- Make installer safe by default.
- Add explicit
--yes-modify-system / --unsafe-system-install for host mutations.
- Add
configure --stdin and interactive secret entry.
- Add transcription resource limits and cleanup by default.
- Make provider fallback explicit with
--allow-paid-fallback.
- Cache doctor/opencli checks and add
doctor --fast.
- Add CI checks: unit tests, ruff, mypy or pyright, dependency scan, and no-network unit test mode.
First two weeks
- Introduce
agent-reach capabilities --json as the machine-readable contract for agents.
- Split
cli.py into focused modules.
- Define typed channel interfaces and contract tests.
- Add a threat model doc focused on agents, cookies, local host mutation, and third-party tools.
- Replace shell-script transcription with Python implementation or mark it experimental.
- Decide and document whether Agent Reach is an installer/capability layer or a unified read/search wrapper.
Suggested regression tests
def test_host_matches_rejects_lookalikes():
assert not GitHubChannel().can_handle("https://github.com.evil.test/x/y")
assert not TwitterChannel().can_handle("https://notx.com/path")
assert not YouTubeChannel().can_handle("https://youtube.com.evil.test/watch?v=x")
def test_config_redacts_cookie_like_values(tmp_path):
cfg = Config(config_path=tmp_path / "config.yaml")
cfg.data = {
"xhs_cookie": "secret",
"twitter_ct0": "secret",
"bilibili_sessdata": "secret",
"groq_api_key": "secret",
}
dumped = cfg.to_dict()
assert all(v != "secret" for v in dumped.values())
def test_doctor_has_no_filesystem_side_effects(monkeypatch, tmp_path):
# doctor should report only; skill install belongs to `agent-reach skill --install`
...
def test_transcribe_temp_files_cleaned_by_default(monkeypatch, tmp_path):
...
def test_unknown_install_channel_errors():
...
Final prioritization
The tie-breaker decision is simple: stop adding integrations until the trust model is fixed.
The order is:
- Safety defaults: installer, secrets, domain validation.
- Truthfulness: docs,
llms.txt, test commands, product boundary.
- Resource controls: transcription, doctor, shell script network/media paths.
- Reliability: Xueqiu cookie path, Docker edge cases, config atomicity.
- Maintainability: split CLI, typed contracts, CI, capability JSON.
Agent Reach is salvageable, but the current version is carrying hidden risk in precisely the areas an agent-enablement tool cannot afford: installation, credentials, and machine-readable instructions.
Agent Reach is not a normal web server, so the main attack surface is not SQL injection or request floods against a public endpoint. The real surface is more dangerous for this product category: local host mutation, supply-chain installation, credential/cookie extraction, subprocess execution, browser profile access, agent-visible docs, and unbounded URL/media processing.
Production readiness: not recommended for broad agent/operator use until P0/P1 issues below are fixed.
Security posture: high-risk defaults, mostly from installer behavior and credential handling.
DX posture: docs/API mismatch is severe enough that LLMs and humans will call commands that do not exist.
UX posture: users are likely to be surprised by filesystem mutations, auto-installs, silent fallbacks, and stale command docs.
Performance posture: acceptable for ad hoc CLI usage, but slow/hang-prone for CI/MCP/bot loops.
Dynamic checks performed:
Config.to_dict()redaction behavior with cookie/session fields.rookiepymodule to validate the Xueqiu cookie loading path.tests/test_core.py::TestAgentReach::test_doctor_report. This suggests the suite mixes unit tests with real doctor/probe behavior and lacks strict global test budgets/mocks.Tooling limitations:
ruff,bandit,pip-audit,semgrep, andmypywere not installed in the container, so this report is primarily manual plus targeted dynamic repros.Severity model
Findings
P0-01 — Installer performs high-risk host mutations and unpinned third-party installs by default
Owners: Security, DX, UX, Team Lead
Files:
agent_reach/cli.pyEvidence:
install --safeexists, but safe mode is opt-in (cli.py:71-74)._install_system_deps()modifies system package sources and installs packages (cli.py:510-635).curland writes to/usr/share/keyringsand/etc/apt/sources.list.d(cli.py:527-545).bash(cli.py:573-583).undici(cli.py:607-609) and later optional channel tools viapipx,uv, ornpm -g(cli.py:680-935).Impact:
A tool whose purpose is to empower agents is automatically modifying the host and adding package sources. That is exactly the path attackers want: compromise a package source, npm package, curl target, DNS, MITM on a misconfigured system, or a local shell environment, and the installer becomes a high-leverage execution chain. Even without compromise, it can break machines by changing Node/npm/apt state.
Tie-breaker verdict: this is the highest-risk issue in the repo. The project’s convenience layer is acting like a bootstrapper with root-level blast radius.
Recommended fix:
--unsafe-system-installor--yes-modify-system.check=Trueor explicit handling.agent-reach install --python-onlyagent-reach install --agents-skillagent-reach install --external-tools --yesagent-reach install --system --yes-i-understand{ "will_modify_system": true, "writes": ["/usr/share/keyrings/...", "/etc/apt/sources.list.d/..."], "commands": ["apt-get update", "npm install -g ..."], "requires_confirmation": true }P0-02 — Credential and cookie handling leaks secrets through CLI args, logs, docs, and incomplete masking
Owners: Security, UX, Edge Case Master
Files:
agent_reach/config.py,agent_reach/cookie_extract.py,agent_reach/cli.py, docsEvidence:
configureaccepts secrets as positional CLI args (cli.py:80-90), which are commonly exposed in shell history, process listings, terminal recordings, LLM tool traces, and CI logs.Config.to_dict()only masks keys containingkey,token,password, orproxy(config.py:102-110). It does not maskxhs_cookie,twitter_ct0,bilibili_sessdata, orxueqiu_cookie.cookie_extract.py:243-289).cookie_extract.py:23-40,cookie_extract.py:137-143).Dynamic repro:
Config.to_dict()maskstwitter_auth_token, but leaks fields like:{ "xhs_cookie": "a=secret; web_session=leak", "twitter_ct0": "csrf-secret", "bilibili_sessdata": "sess-secret", "xueqiu_cookie": "xq_a_token=secret" }Impact:
These values are account credentials in practice. A leaked cookie or CSRF/session token can let another process or person act as the user, scrape authenticated content, or burn paid API quota. Because this is agent-facing software, “printed config” and “copied command” are not minor risks; they are normal operating paths.
Recommended fix:
getpassfor interactive secrets.--show-secretsonly with an explicit danger prompt.P1-03 — Domain routing accepts malicious lookalike hosts
Owners: Security, Edge Case Master, UX
Files:
agent_reach/channels/*.pyEvidence:
Multiple channel handlers use substring checks against
urlparse(url).netloc.lower().Examples:
github.py:17twitter.py:17youtube.py:33reddit.py:45bilibili.py:44linkedin.py:20v2ex.py:33xueqiu.py:158xiaohongshu.py:158Dynamic repro:
The following malicious or unrelated domains are currently accepted as trusted platform URLs:
Impact:
A malicious URL can be routed into a platform-specific handler. Depending on the handler, this can cause wrong backend selection, credential/cookie use against attacker-controlled hosts, privacy leaks, or user/agent confusion. It is especially dangerous because the product is designed for agents that may classify and fetch URLs automatically.
Recommended fix:
Centralize URL host validation:
Then update handlers:
Add explicit tests for negative lookalike hosts.
P1-04 — Transcription path allows resource exhaustion, privacy/cost surprises, and leftover temporary data
Owners: Security, Performance, UX
Files:
agent_reach/transcribe.pyEvidence:
yt-dlpis called on arbitrary URL input with no--no-playlist, no max file size, no max duration, and a 30-minute timeout (transcribe.py:77-98).transcribe.py:230-234) with no size or duration limit.tempfile.mkdtemp(prefix="transcribe-")and intentionally left behind (transcribe.py:214-228).TranscribeError(transcribe.py:249-261).Impact:
A user or agent can accidentally or maliciously transcribe an enormous playlist, a very long video, a local sensitive file, or many chunks. This can burn API quota, fill disk, consume CPU via ffmpeg, and leak media to a paid third-party provider the user did not expect. The temp-dir behavior leaves source/compressed/chunked audio on disk.
Recommended fix:
TemporaryDirectory()by default and only preserve intermediates under--keep-temp.yt-dlpflags:ffprobebefore processing.P1-05 —
doctorhas side effects and can become slow/hang-prone in tests, CI, and MCP contextsOwners: Performance, UX, DX
Files:
agent_reach/cli.py,agent_reach/doctor.py,agent_reach/backends/opencli.py,agent_reach/integrations/mcp_server.pyEvidence:
_cmd_doctor()calls_install_skill()after generating the doctor report (cli.py:1447-1464). A diagnostic command mutates the filesystem.opencli_status()shells out toopencli --versionandopencli daemon statusand scans browser extension dirs (backends/opencli.py:63-136). Multiple channels can call this repeatedly.get_statushandler calls synchronousdoctor_report()directly (integrations/mcp_server.py:44-53).Impact:
Diagnostics should be safe, fast, and side-effect-free. Here they install skills, touch multiple directories, shell out, and perform network/backend checks. In an agent loop or MCP server, this can turn a status call into a latency spike or filesystem mutation. In CI, it makes otherwise simple tests brittle and slow.
Recommended fix:
doctor.P1-06 —
scripts/transcribe_xiaoyuzhou.shuses predictable tmp paths and unbounded network/media operationsOwners: Security, Performance, Edge Case Master
Files:
scripts/transcribe_xiaoyuzhou.shEvidence:
TMPDIR="/tmp/xiaoyuzhou_$$"(scripts/transcribe_xiaoyuzhou.sh:28).curl -s "$URL"with no timeout, no max size, and no--fail(scripts/transcribe_xiaoyuzhou.sh:55).curl -sL -owith no timeout or size bound (scripts/transcribe_xiaoyuzhou.sh:70).scripts/transcribe_xiaoyuzhou.sh:34).ffprobe,ffmpeg, andbc, but does not check all dependencies up front./tmp(scripts/transcribe_xiaoyuzhou.sh:27,:262).Impact:
A predictable temp path creates collision/symlink/race risk on multi-user machines. Unbounded network/media processing can hang, fill disk, or burn CPU. The script is also more fragile than the Python transcription module and should not be the hardened path.
Recommended fix:
Use bounded curl:
Pass config path through environment:
Prefer porting this script into the Python CLI and deprecating the shell script.
P1-07 — Docs and agent-facing API describe commands that do not exist
Owners: DX, UX, Team Lead
Files:
llms.txt,test.sh,CLAUDE.md,README.md,docs/*,agent_reach/cli.pyEvidence:
setup,install,configure,doctor,uninstall,skill,format,transcribe,check-update,watch,version(cli.py:50-133).llms.txttells agents to use commands likeagent-reach read <url>andagent-reach search-twitter "query"(llms.txt:13).test.shinvokes nonexistent commands:agent-reach read,search,search-github,search-twitter,search-reddit,search-youtube,search-bilibili,search-xhs(test.sh:58-74).CLAUDE.mdsays every channel implementsread(url),search(query), andcheck(), but the base channel contract does not enforce this, and the actual CLI does not expose those commands.Impact:
This is not cosmetic. This product is explicitly agent-facing. Stale
llms.txtand test docs are an API bug because agents will read them and call invalid commands. Users will also conclude the install is broken when the actual code is merely narrower than the docs claim.Recommended fix:
Choose one product boundary:
read/searchclaims from docs and tests. Publish exact backend command templates instead.agent-reach read,agent-reach search, and per-platform search commands with a stable channel interface.Do not leave it halfway. Halfway is the worst option: it maximizes support burden and minimizes trust.
Recommended agent-facing endpoint:
Example output:
{ "version": "0.x.y", "commands": ["doctor", "configure", "transcribe"], "channels": { "github": {"status": "available", "backend": "gh", "read_template": "gh api ..."}, "twitter": {"status": "missing", "install_hint": "agent-reach install --channels twitter"} } }P2-08 — Xueqiu browser-cookie loading path is silently broken
Owners: Edge Case Master, Security, UX
Files:
agent_reach/channels/xueqiu.pyEvidence:
_load_cookies_from_browser()builds ahttp.cookiejar.CookieJar, then callsself.session.cookies.set(...)(xueqiu.py:81-89).CookieJardoes not expose therequests.cookies.RequestsCookieJar.setmethod.Dynamic repro:
Injecting a fake
rookiepythat returns anxq_a_tokencookie causes_load_cookies_from_browser()to catch an exception and returnFalse. The cookie jar remains empty. Because the function catches broad exceptions, users receive no actionable diagnosis.Impact:
A documented authentication path fails silently. Users fall back to unauthenticated behavior, confusing rate limits, or manual cookie config. Security-wise, broad silent failure makes it harder to distinguish “no cookies found” from “cookie code is broken.”
Recommended fix:
Use
requests.Session().cookiesas aRequestsCookieJar, or construct properCookieobjects forCookieJar. Prefer the simpler route:Add a regression test with fake
rookiepy.P2-09 — Config writes are permission-conscious but not symlink-safe or atomic
Owners: Security, DX
Files:
agent_reach/config.py,agent_reach/cookie_extract.pyEvidence:
mkdir(parents=True, exist_ok=True)without setting0700(config.py:37-39).os.open(..., O_WRONLY | O_CREAT | O_TRUNC, 0o600)(config.py:54-62), but do not useO_NOFOLLOWwhere available and are not atomic.open()(config.py:63-67).open()fallback (cookie_extract.py:151-170).Impact:
The file mode is good for the common case. But a local attacker or broken environment can exploit symlinks or partial writes. Atomicity matters because these files contain credentials and agents may read them while they are being written.
Recommended fix:
0700on create and warn if broader.0600, thenos.replace().O_NOFOLLOWwhere available.P2-10 — Docker cookie update path has temp-file and container-selection edge cases
Owners: Security, Edge Case Master, UX
Files:
agent_reach/cli.pyEvidence:
_configure_xhs_cookies()detects a Docker container bydocker ps --filter name=xiaohongshu-mcp --format {{.Names}}and strips stdout (cli.py:1263-1270). If multiple containers match, the name can contain newlines.NamedTemporaryFile(delete=False)and unlinked only afterdocker cpsucceeds (cli.py:1291-1303). Exceptions before unlink leave cookie material in/tmp.cli.py:1310-1320) without an exact-match prompt.Impact:
This is mostly a local footgun, but it deals with live account cookies. Multi-container environments, failed
docker cp, or weird container names can leave secrets on disk or restart the wrong service.Recommended fix:
try/finallyaround temp unlink.docker cp -or bind-mounted config paths to avoid temp cookie files.P2-11 — Some HTTP reads and query parameters are unbounded or not encoded
Owners: Security, Performance, Edge Case Master
Files:
agent_reach/channels/v2ex.py,agent_reach/channels/xueqiu.py, other channel modulesEvidence:
node_nameandusernameinto paths without URL encoding (v2ex.py:89-93,v2ex.py:175-177).xueqiu.py:196,xueqiu.py:218-230,xueqiu.py:290-303).Impact:
This is not catastrophic in a CLI, but it is agent-facing code. Agents will pass malformed, adversarial, or copy-pasted values. Unbounded response and parameter handling creates weird failures, slow runs, excessive output, and possible backend abuse.
Recommended fix:
urllib.parse.quote.limitarguments with per-method caps.P2-12 — Optional dependency constraints are not enforced by package installation
Owners: Security, DX
Files:
pyproject.toml,constraints.txtEvidence:
pyproject.tomluses broad dependency lower bounds such asyt-dlp>=2024.0andrequests>=2.28.constraints.txtpins concrete versions, but normal package installation will not use it unless the user explicitly installs with-c constraints.txt.yt-dlp==2025.5.22, while the Python package allows much older or newer versions.Impact:
The project has two dependency stories: broad runtime ranges and a constraint file most users will never apply. That makes local behavior hard to reproduce and weakens supply-chain control.
Recommended fix:
P3-13 —
SECURITY.mdexcludes dependency vulnerabilities, but dependencies are the product’s highest-risk layerOwners: Security, DX
Files:
SECURITY.mdEvidence:
SECURITY.mdsays dependency vulnerabilities are out of scope unless they directly impact project code.Impact:
For this repository, external tools and packages are not incidental. The product installs and orchestrates them. Dependency and supply-chain vulnerabilities should be first-class security reports.
Recommended fix:
Accept reports for dependencies when Agent Reach pins, recommends, installs, shells out to, or passes credentials through them.
P3-14 —
Config.get()prioritizes config file over environment variablesOwners: UX, DX
Files:
agent_reach/config.pyEvidence:
Config.get()checksself.datafirst, then environment variables (config.py:69-78).Impact:
Many CLI users expect env vars to override config files for one-off runs and CI. Current behavior can surprise users who believe
GROQ_API_KEY=... agent-reach transcribe ...overrides a stale file value.Recommended fix:
Either reverse precedence or document it clearly and add
AGENT_REACH_CONFIG_PRECEDENCE=file|env.P3-15 — Version comparison is simplistic
Owners: DX
Files:
agent_reach/cli.pyEvidence:
_is_newer_version()parses dotted digits only (cli.py:1658-1674).Impact:
Pre-releases, post-releases, build metadata, and non-standard tags may compare incorrectly.
Recommended fix:
Use
packaging.version.Version.Expert pass summaries
Security expert
Primary concern: the project handles credentials and tells users/agents to install external tooling, but the trust boundaries are not explicit. The installer is too powerful by default, and the credential redaction model is incomplete.
Security priorities:
Exploit classes considered:
Performance expert
This code is probably fine for a human running one command occasionally. It is not fine for agents polling status, MCP clients calling status, or CI. The doctor path is too heavy, repeated OpenCLI checks should be cached, and transcribe has no practical resource budget.
Performance priorities:
doctor --fastlocal-only and cache expensive checks.limitparameters and response body sizes.UX expert
The worst UX issue is not visual. It is broken expectation management. The docs say commands exist that do not exist.
doctormutates the filesystem.installperforms system changes unless the user knows to pass--safe.transcribe --provider autocan switch providers silently.UX priorities:
DX expert
The project is currently LLM-hostile despite being agent-oriented.
llms.txtis stale,CLAUDE.mddescribes contracts that are not enforced, andcli.pyis doing too much in one 1,800-line module.DX priorities:
cli.pyinto install/config/doctor/skill/transcribe modules.Edge Case Master
The interesting bugs are not exotic; they are “obvious after seeing them”:
"x.com" in "notx.com"routesnotx.comas Twitter/X.github.com.evil.testroutes as GitHub.Config.to_dict()maskstoken, but notcookie,sessdata, orct0..set()on aCookieJarand then swallows the exception./tmp/xiaoyuzhou_$$for temp data.Edge-case priorities:
llms.txtandtest.sh.Dependency freshness notes
This is not a full dependency audit, but these checks matter:
constraints.txtpinsyt-dlp==2025.5.22. A later NVD entry reports a Windows command-injection vulnerability affectingyt-dlpversions2025.06.25and below when--execis used, fixed in2025.07.21. Agent Reach does not appear to use--exec, so this is not directly exploitable from currenttranscribe.py, but the pinned version is stale and within the affected range.yt-dlprelease than the pinned constraint.Recommended action: add automated dependency scanning and decide whether constraints are authoritative.
Immediate patch plan
First 24 hours
doctor.llms.txt,test.sh, and docs to remove nonexistent commands or clearly mark them as planned.mktemp -dand add bounded curl flags.--channels; fail or warn on unknown names.First week
--yes-modify-system/--unsafe-system-installfor host mutations.configure --stdinand interactive secret entry.--allow-paid-fallback.doctor --fast.First two weeks
agent-reach capabilities --jsonas the machine-readable contract for agents.cli.pyinto focused modules.Suggested regression tests
Final prioritization
The tie-breaker decision is simple: stop adding integrations until the trust model is fixed.
The order is:
llms.txt, test commands, product boundary.Agent Reach is salvageable, but the current version is carrying hidden risk in precisely the areas an agent-enablement tool cannot afford: installation, credentials, and machine-readable instructions.