Skip to the content.

huggingface/tau — security scan

Repository: huggingface/tau Commit scanned: 261662b (main) Scan date: 2026-10-23 Disclosure status: post-only — nothing first-party survived curation, so nothing was filed upstream. All 42 dependency advisories sit in uv.lock, which none of the documented user installs reads.

Summary

Severity Count
Critical 6
High 26
Medium 14
Low 0
Info 3

Total findings: 49 at --min-severity medium (0 first-party defects after curation; 42 dependency advisories, all from the lockfile; the 3 info rows are coverage meta findings)

tau is a Python port of Pi’s minimalist coding-agent harness, published on PyPI as tau-ai (0.4.8 at the scanned commit): a Textual terminal UI, an RPC mode, an extension system, project skills and prompts, session export, and sign-in with an OpenAI Codex subscription alongside API keys (2,984★, MIT). It is young and busy: created in June 2026, with 63 merged pull requests from 11 human authors and 21 closed issues in the 60 days before the scan.

Scope of this write-up. Every finding family below was checked at its call site, and five surfaces no rule inspects were read by hand: the one listening socket (the OAuth callback), the project-trust gate, the session HTML export, the image-decoding path and the self-updater’s command form. Not swept by hand: the provider adapters, the API offered to trusted extensions, the RPC protocol, how the TUI renders model and tool output, and tui/widgets.py, which Semgrep could not parse (below). Read the result as “the tools’ 49 findings are accounted for, plus those five surfaces”, not as a clean bill of health for the rest.

Scan coverage

Tool Status Detail
semgrep partial Semgrep did not cover every file it was pointed at
gitleaks ran Ran without reporting a coverage problem.
trivy ran Ran without reporting a coverage problem.
dependency-scan partial A shipped lockfile was not covered by the dependency scan
ai-security-review not_run AI security review is disabled

Coverage complete: no. Semgrep scanned 384 paths but could not parse src/tau_coding/tui/widgets.py: line 3061 declares a Python 3.12 constrained type parameter (def _grouped_sidebar_file_widgets[SidebarResource: (Skill, PromptTemplate)](), so no Semgrep rule ran on that file. Its two timeouts were the boto3 hardcoded-token rule on session_export.py and tui/app.py, in a project that does not import boto3; every other rule completed on both files, and Gitleaks reads all three. dependency-scan (pip-audit) resolved the version floors in pyproject.toml and did not read the shipped uv.lock; Trivy read uv.lock, and every dependency finding below comes from it.

Top findings

There is no first-party finding to lead with. The one item worth acting on is a lockfile that has fallen behind, and it reaches fewer people than its 42 rows suggest.

1. uv.lock pins Pillow 12.3.0, anyio 4.13.0 and certifi 2026.5.20 — the contributor install only

Why the rest were dismissed

All 46 non-meta findings are accounted for below; the other 3 are the coverage meta findings above. The scanner dropped nothing: no finding fell below the --min-severity floor, and none sat under an ignored sample path.

Reason Findings
dependency-currency (finding 1) 42
by-design 3
not-reachable 1

The three by-design rows are Semgrep’s python37-compatibility-importlib2, which warns that importlib.resources does not exist before Python 3.7 in a project that requires 3.12. The not-reachable row is dynamic-urllib-use-detected in scripts/generate_models.py:26, a maintainer build script for the model catalogue: its loader hands only http:// and https:// locations to urlopen, so the file:// read the rule warns about cannot reach it, and the line already carries a reviewed noqa: S310.

Patterns observed

The coding-agent risk that matters most is gated, and gated wide. A coding agent opened in someone else’s repository reads whatever that repository ships for it. tau’s project-trust layer (src/tau_coding/project_trust.py) covers seven kinds of project-supplied input: context (AGENTS.md at every level from the project root down to the working directory, plus .tau/AGENTS.md and .agents/AGENTS.md), extensions (.tau/extensions, Python that runs at session startup), prompts, settings, skills, system prompts (.tau/SYSTEM.md, .tau/APPEND_SYSTEM.md) and themes. In the CLI, none of it loads until the project is trusted. The default is to ask, and a session that cannot ask — headless, or with no UI to ask in — resolves to untrusted. The project path is canonicalized with resolve(strict=True) before trust is resolved. Gating AGENTS.md is the strict choice: it is easy to treat as harmless documentation, and it is the file a hostile repository would write first. The module states its own limit in one sentence: project trust “is deliberately not a filesystem, process, network, tool, model, or prompt-injection sandbox”. That sentence matters. A trust prompt that implied containment would be the finding; one that disclaims it is an honest boundary. Past the gate, tau does what the Pi design it ports does: the model’s tool calls, shell included, run without a per-call approval step, and nothing in the README or the code claims otherwise. For a coding agent that is product surface, not a defect.

One listening socket, built the careful way. The only server socket in the codebase is the OAuth callback for signing in with an OpenAI Codex subscription. It binds 127.0.0.1 by default (TAU_OAUTH_CALLBACK_HOST overrides it), the flow uses PKCE with an S256 challenge and a secrets.token_hex(16) state, and the callback handler rejects a missing or wrong state. The one lenient check is deliberate: when the user pastes the redirect URL or a bare code by hand, _validate_state accepts input that carries no state, because a bare code has none. Elsewhere in this series that shape is the finding: a check skipped whenever the caller leaves the field out. Here PKCE retires it. A code is redeemable only with the verifier this process generated, so a code minted by anyone else’s login fails at the token endpoint.

Output that leaves the machine is escaped. Session export writes a standalone HTML file. Text goes through _escape and attributes through _attr. The session itself is embedded base64-encoded in a <script type="application/octet-stream"> block, which no base64 string can close, and read back with textContent. session_export.py never assigns innerHTML and loads no external script or stylesheet. A transcript is attacker-influenced by nature — it holds whatever a fetched page or a repository’s files said — so this is the right default for a file people share.

The drift is where automation does not reach. Dependabot runs weekly for GitHub Actions with a seven-day cooldown, and the day’s newest commit was one of its grouped updates. Nothing watches uv.lock, which is why the lockfile carries three packages past their fixes while every user install is current. It is the same gap voxbento had in its docs-site npm lockfile. tau has no SECURITY.md, but private vulnerability reporting is enabled on the repository, so a reporter does have a private channel.

Notes on the tool

Disclosure timeline

Reproduce

git clone https://github.com/huggingface/tau /tmp/scan-target
python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/huggingface-tau --min-severity medium --ignore-samples

More from this series