| Version | Supported |
|---|---|
| Latest release on PyPI (currently 0.36.0) | Yes |
| Older releases | No |
Only the latest published release receives security fixes. Please upgrade before reporting. Since v0.36.0, the 0.36.x line is the supported one and 0.35.x no longer receives fixes.
Do not open a public issue. Instead, use one of these channels:
- Email: security@littlebearapps.com
- GitHub Security Advisories: Report a vulnerability
Include:
- Description of the vulnerability
- Steps to reproduce
- Untether version and Python version
- Any relevant logs (redact credentials)
| Step | Target |
|---|---|
| Acknowledgement | Within 48 hours |
| Initial assessment | Within 5 business days |
| Fix for critical issues | Within 7 days |
| Public disclosure | After fix is released |
In scope:
- Untether application code
- Configuration parsing and validation
- Telegram transport security
- Subprocess management and session handling
Out of scope:
- Upstream agent CLIs (Claude Code, Codex, OpenCode, Pi) — report to their respective maintainers
- Telegram Bot API — report to Telegram
- Bot token management — token security is the operator's responsibility
- Issues requiring physical access to the host machine
v0.36.0 (published to TestPyPI as release candidates 0.35.5rc1–rc20, then 0.36.0rcN) closes several approval, sandbox and file-access gaps. Upgrade notes:
- BREAKING —
extra_argsrefuses approval and sandbox bypass flags (#209). Claude's--dangerously-skip-permissions,--allowedTools,--permission-promptsand similar, and Codex's--yolo,--dangerously-bypass-approvals-and-sandbox,--sandbox danger-full-access,-C/--cdand similar, now fail config load: the default engine won't start and any other engine is disabled until the flag is removed. Errors and logs name the flag, never its value.[engines.claude] dangerously_skip_permissions = truenow logs a warning, since it overrides every/planmodechoice. See Security how-to → Engine CLI flags for whatextra_argsblocking can't stop. - Codex safe mode now restricts Codex (#830). Before v0.36.0,
codex execignored the approval flag Untether passed, so a chat set to safe ran exactly like full auto (and on codex-cli 0.149.0+ every safe run failed to start). Safe now uses Codex's read-only sandbox: edits, writes and shell network access are blocked. - Claude prompting modes prompt (#749).
default,manualandacceptEdits(/planmode off) used to approve every tool silently; anything the mode doesn't cover now waits for an Approve / Deny tap. Unattended crons and webhooks in these modes are flagged in the log (trigger.unattended_approval_risk), and their requests are denied rather than left waiting (next bullet). - BREAKING — unattended trigger runs fail closed (#835). A cron or webhook Claude run never waits on an approval nobody can give, and never approves what an attended run would have asked about: tool approvals, plan approvals and questions are denied at once (
permission.unattended_deny, listed in the run's final), and inauto,dontAskandbypassPermissionsevery request that still reaches Untether is denied.planand prompting-mode crons (including crons with nopermission_mode, which inheritplanby default) now end with a plan or a report instead of acting; give each Claude cron that should act on its own an explicitpermission_mode. A reply to an unattended run continues in an attended session with normal buttons. - Approval buttons are bound to their chat (#388). A forged
claude_control:callback sent from another chat is refused, so a pending approval can only be answered where it was posted. - Opt-in: only the run's originator can answer (#388). In a chat with several allowed users, any of them can answer any approval by default. Set
[transports.telegram] approval_originator_only = true(hot-reloads) and Claude Code approval buttons, the background-agent approval message and AskUserQuestion answers are accepted only from the person whose message started the run; anyone else gets "Only the person who started this run can answer this." and a WARNING (callback.not_originator/ask_user_question.not_originator). Cron, webhook,/atand loop runs have no originator, so any allowed user can still answer them. See Group chat → Only the person who started the run can approve. - OpenCode 2.x is refused before it starts (#970). OpenCode 2.x (
@opencode/cli) runs prompts on a shared per-user background service, outside Untether's environment allowlist, working directory, cancel and stall watchdog. Untether now checksopencode --versionbefore each run and refuses 2.x with an install hint for the supported 1.x CLI (opencode-ai), keeping the chat's saved session. - No silent approvals when Claude Code downgrades
auto(#751). On a model without auto mode (such as Haiku), Claude Code quietly runsautoasdefault; Untether used to approve those permission requests automatically. It now shows⚠️ Asked for auto mode — Claude Code is running default, logsclaude.permission_mode.mismatchand sends the requests to Telegram. - File deny globs match at the project root and at any depth (#831). With the default
deny_globs, a project-rootkey.pem,id_rsa,.env.local,.npmrc,.netrcor.ssh/configwas not denied, and nothing deeper than one level under.ssh/was. Matching is now strictly more denying for/file get,/file put, outbox delivery,/browse, webhookfile_writeand cronfile_read; the.gitrule is case-insensitive. A project-root.env.exampleis now denied too. - Outbox delivery stays inside the project and sends only fresh files (#924). Only files written or copied into
.untether-outbox/during the run are sent; leftovers from earlier sessions are archived to.untether-outbox/.skipped/instead of being attached to an unrelated answer. Anoutbox_dircontaining..now fails config load, and an outbox that resolves outside the project (for example a symlinked.untether-outbox) is never scanned or archived. - Deny globs follow symlinks (#390).
/file putand/file getcheck the path a request resolves to as well as the path requested, so an in-root symlink (e.g.docs/x→.git/hooks) can no longer route around.git/**or.env. /browseneeds a project root (#389, #210). It no longer falls back to the process working directory ($HOMEunder systemd), applies the deny globs and hidden-path rules to listings and previews, resolves symlinks before its containment check, and scopes its button ids per chat.- Log redaction widened (#800, #679). Process titles are scanned in full, and bearer credentials, JWTs and
api_key=/token=/secret=/password=values are redacted;ssrf.*log lines redact URL userinfo. - Dependency advisories —
anyio4.15.1 (CVE-2026-63374, CVE-2026-64847; #773) andaiohttp3.14.3 (PYSEC-2026-3545/3546/3547) in the lockfile.
See the CHANGELOG (v0.36.0 section) for the full entry list.
v0.35.3 ships a follow-on hardening bundle on top of v0.35.2. Upgrade notes:
- BREAKING — empty
allowed_user_idsrejected at startup (#377). Previously the empty default meant any Telegram user who knew the bot username could send commands. Untether now refuses to start withConfigError: [transports.telegram] allowed_user_ids is empty …. Operators who genuinely need an open bot (demos, hackathons, dev) must opt in explicitly withallow_any_user = true, which is logged INFO every boot (security.allow_any_user). See Security how-to. - AMP
dangerously_allow_alldefault flipped tofalse(#206). AMP runs no longer skip its built-in permission system unless the operator opts in. - Pi session directory locked to
0o700(#207). Other users on shared hosts can no longer read Pi session JSONL. voice_transcription_api_keyis nowSecretStr(#378) — parity withbot_token. Masked in repr/str/tracebacks and structlog serialisation.- Prompt content removed from INFO logs (#205, #478) —
runner.startno longer carriesprompt[:100]. A debug-onlyrunner.start_promptevent is available when explicitly enabled. /file getTOCTOU window closed (#211) — single-open + bounded read in a worker thread.- stderr sanitisation regex extended (#208) — covers macOS (
/Users/…,/private/var/…), container roots (/app/,/workspace/), and other absolute paths beyond/home/<user>/. - OpenAI project-key redaction (#213) — structlog redaction now covers
sk-proj-…keys (the genericsk-…regex didn't match the project-key char set). - Daily cost tracker race fixed (#379) — the unguarded read-modify-write that could lose a run's cost (and bypass the per-day budget cap) is now wrapped in a lock.
- Pygments bumped 2.19.2 → 2.20.0 (#402) — clears CVE-2026-4539 (ReDoS in
AdlLexer). - Auto-approve scope re-audit (#380) —
ControlRewindFilesRequestandControlMcpMessageRequestre-verified safe under the upstream Claude Code 2.1.x trust model. Regression-lock tests fail loudly if the auto-approve path starts inspecting payloads. Audit memo atdocs/audits/2026-04-27-380-auto-approve-scope-review.md. - User-extensible env allowlist (#409) —
[security] env_extra_allowandenv_extra_prefix_allowlet operators thread credential-manager tokens (1Password, Doppler, Vault, Infisical) into engine subprocesses without forking.BWS_ACCESS_TOKENis now in the built-in defaults.
See CHANGELOG v0.35.3 for the full entry list.
v0.35.2 ships a security hardening bundle. Upgrade notes:
- Env allowlist for Claude/Pi subprocesses — only approved variables pass through; unrelated process env no longer leaks to agent CLIs. (#198)
- Runtime env hardening (always on) — Claude exec is wrapped with
env -i KEY=VAL …so the resolved environment is exactly the allowlist fromutils/env_policy.filtered_env(), even if an upstream rc-file source or wrapper script would otherwise re-introduce host vars aftersubprocess.spawn(env=…)is honoured. This hardening is not controlled by any config setting. - Runtime env audit — gated by
[security] env_audit = true(default). Samples/proc/<claude_pid>/environonce per session and emits aclaude.env_audit.leaked_varWARNING for every non-allowlisted variable observed. Disabling the audit only silences the warning sampler — it does not disable theenv -ihardening above. (#361) bot_tokenstored asSecretStr— masked in repr/str/tracebacks; unwrapped only at the transport boundary. (#196)- User-safe error messages — voice transcription and command-dispatch failures route through
user_safe_error()(strips URLs/paths, caps length, fallback on empty). (#200, #201) - Codex auth output HTML-escaped — prevents entity injection before
<pre>wrapping. (#199) - Download URL path validation — blocks
://,.., and leading/before URL construction. (#204) - Duplicate-request dedup via LRU — bounded
OrderedDict(max 200) closes a small race that the previous wholesale-clear approach left open. (#197) - Registry ephemeral sweep —
_EPHEMERAL_MSGS/_OUTLINE_REGISTRYentries older than 1 hour are pruned on a 60 s tick. (#203) - CI matrix interpolation moved to
env:— eliminates a shell-injection vector in the release pipeline. (#195) - Subprocess sites annotated inline — global
B603/B607bandit skips removed; each call site carries its own# nosecjustification. (#202)
See CHANGELOG v0.35.2 for the full entry list.
We follow coordinated disclosure. We ask that you:
- Allow us reasonable time to investigate and fix the issue
- Do not exploit the vulnerability beyond what is needed for the report
- Do not disclose publicly until a fix is available
We credit reporters in the release notes (unless you prefer to remain anonymous).