Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pipx install untether # alternative
untether # run setup wizard
```

Update: `uv tool upgrade untether` · Uninstall: `uv tool uninstall untether && rm -rf ~/.untether/`

The wizard creates a Telegram bot, picks your workflow, and connects your chat. Then send a message to your bot:

> fix the failing tests in src/auth
Expand Down Expand Up @@ -273,6 +275,8 @@ Full documentation is available in the [`docs/`](https://github.com/littlebearap
- [Group chats](https://github.com/littlebearapps/untether/blob/master/docs/how-to/group-chat.md) — multi-user and trigger modes
- [Context binding](https://github.com/littlebearapps/untether/blob/master/docs/how-to/context-binding.md) — per-chat project/branch binding
- [Webhooks and cron](https://github.com/littlebearapps/untether/blob/master/docs/how-to/webhooks-and-cron.md) — automated runs from external events
- [Update Untether](https://github.com/littlebearapps/untether/blob/master/docs/how-to/update.md) — upgrade to the latest version
- [Uninstall Untether](https://github.com/littlebearapps/untether/blob/master/docs/how-to/uninstall.md) — remove CLI, config, and state files

### Engine Guides

Expand All @@ -291,6 +295,26 @@ Full documentation is available in the [`docs/`](https://github.com/littlebearap

---

## 🔒 What Untether accesses

Untether runs on your machine and bridges your agents to Telegram. Here's exactly what it touches:

| Category | What | Details |
|----------|------|---------|
| **Network** | Telegram Bot API (`api.telegram.org`) | Core transport — always active during operation |
| **Network** | Whisper-compatible endpoint | Voice transcription — **disabled by default**, opt-in via config |
| **Network** | Agent APIs (Anthropic, OpenAI, etc.) | Called by agent subprocesses, not by Untether directly |
| **Filesystem** | `~/.untether/untether.toml` | Config file containing bot token — protect with `chmod 600` |
| **Filesystem** | `~/.untether/*.json` | Chat preferences, session state, usage stats |
| **Filesystem** | `.untether-outbox/` | Agent-delivered files (optional, per-project) |
| **Processes** | Agent CLIs (claude, codex, etc.) | Spawned as subprocesses with your user permissions |
| **Credentials** | Telegram bot token | Stored in config file (plaintext TOML) |
| **Credentials** | API keys | Read from environment variables, never stored by Untether |

**What Untether does NOT do:** no telemetry, no analytics, no phone-home, no auto-updates, no root access, no system file modifications outside `~/.untether/`. Sensitive tokens are automatically [redacted from logs](https://github.com/littlebearapps/untether/blob/master/docs/how-to/security.md).

---

## 🤝 Contributing

Found a bug? Got an idea? [Open an issue](https://github.com/littlebearapps/untether/issues) — we'd love to hear from you.
Expand Down
6 changes: 6 additions & 0 deletions docs/how-to/group-chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ Then send a message — Untether prints the chat ID and your user ID.

In group chats, each user gets their own independent session. User A's conversation history and context are completely separate from User B's — there is no cross-talk between sessions.

## Button press validation

In group chats, approval buttons (Approve, Deny, Pause & Outline Plan) are validated against `allowed_user_ids`. If a group member who is not in the allowed list taps another user's approval buttons, the press is rejected — they cannot approve or deny tool calls on someone else's behalf.

This also applies to cancel buttons. When `allowed_user_ids` is empty (the default), all group members can interact with any buttons.

## Set trigger mode for groups

By default, the bot responds to every message (`all` mode). In busy groups, switch to `mentions` mode so the bot only responds when @mentioned:
Expand Down
2 changes: 2 additions & 0 deletions docs/how-to/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ If you need exact options and defaults, use **[Reference](../reference/index.md)
## Getting started

- [Choose a workflow mode](choose-a-mode.md) (assistant, workspace, or handoff — pick the style that fits)
- [Update Untether](update.md) (upgrade to the latest version)
- [Uninstall Untether](uninstall.md) (remove CLI, config, and state)

## Daily use

Expand Down
3 changes: 3 additions & 0 deletions docs/how-to/interactive-approval.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ This lets you make informed approve/deny decisions without leaving Telegram.

<img src="../assets/screenshots/approval-diff-preview.jpg" alt="Approval message with compact diff preview showing removed and added lines" width="360" loading="lazy" />

!!! note "After plan approval"
When you approve a plan outline (see [Plan mode](plan-mode.md#auto-approval-after-plan-approval)), diff previews are skipped for the rest of the session — tools are auto-approved since you already reviewed the plan.

## Answering questions

When Claude Code calls `AskUserQuestion`, Untether renders the question with interactive option buttons in Telegram:
Expand Down
6 changes: 6 additions & 0 deletions docs/how-to/plan-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ This prevents the agent from bulldozing through when you've asked it to slow dow

</div>

## Auto-approval after plan approval

Once you approve a plan outline (via "Approve Plan"), subsequent tool calls in the same session — Edit, Write, Bash — are auto-approved without showing individual diff preview buttons. You have already reviewed the plan, so per-tool approval is skipped.

This applies whether you approve via "Approve Plan" after an outline or by directly approving an ExitPlanMode request. Starting a new session (via `/new` or a new message) restores normal approval behaviour.

## Related

- [Interactive approval](interactive-approval.md) — how approval buttons and diff previews work
Expand Down
7 changes: 5 additions & 2 deletions docs/how-to/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ By default, anyone who can message your bot can start agent runs. To restrict ac
allowed_user_ids = [12345, 67890]
```

When this list is non-empty, only the listed user IDs can interact with the bot. Messages from everyone else are silently ignored.
When this list is non-empty, only the listed user IDs can interact with the bot. Messages from everyone else are silently ignored. In group chats, `allowed_user_ids` also governs button press validation — unauthorised users cannot tap Approve/Deny buttons on another user's tool requests. See [Group chat](group-chat.md#button-press-validation) for details.

To find your Telegram user ID:

Expand Down Expand Up @@ -50,6 +50,9 @@ If you store your config in a non-standard location, set the `UNTETHER_CONFIG_PA
export UNTETHER_CONFIG_PATH=/path/to/untether.toml
```

!!! tip "Automatic log redaction"
Untether automatically redacts bot tokens, OpenAI API keys (`sk-...`), and GitHub tokens (`ghp_`, `ghs_`, `github_pat_`) from all structured log output. Even if a token appears in engine output or error messages, it is replaced with `[REDACTED]` before being written to logs.

## File transfer deny globs

File transfer includes a deny list that blocks access to sensitive paths. The defaults are:
Expand Down Expand Up @@ -132,7 +135,7 @@ Trigger features that make outbound HTTP requests (webhook forwarding, cron data

DNS resolution is checked after hostname lookup to prevent DNS rebinding attacks (hostname resolves to a private IP).

If you need triggers to reach local services, you can configure an allowlist (see the [triggers reference](../reference/triggers/triggers.md)).
If you need triggers to reach local services, route traffic through a reverse proxy on a non-private address. The SSRF allowlist is available as a code-level parameter in `triggers/ssrf.py` but is not currently exposed as a TOML setting.

## Untrusted payload marking

Expand Down
4 changes: 4 additions & 0 deletions docs/how-to/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ If OpenCode emits a JSONL event type that Untether doesn't recognise (e.g. a `qu

If you see this warning, check for an Untether update that adds support for the new event type. OpenCode's `run` command auto-denies questions via permission rules, so this should be rare — it most likely indicates an OpenCode protocol change.

## Engine output line cap

Individual engine stdout lines are capped at 10 MB. If an engine emits a single JSONL line exceeding this limit (e.g. a very large base64 image in a tool result), the line is truncated and a warning is logged. This prevents unbounded memory growth from malformed engine output.

## Stall warnings

**Symptoms:** Telegram shows "⏳ No progress for X min — session may be stuck" or "⏳ MCP tool running: server-name (X min)".
Expand Down
65 changes: 65 additions & 0 deletions docs/how-to/uninstall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Uninstall Untether

## 1. Stop the service

If Untether is running as a systemd service, stop it first:

```sh
systemctl --user stop untether
systemctl --user disable untether
```

## 2. Remove the CLI

=== "uv"

```sh
uv tool uninstall untether
```

=== "pipx"

```sh
pipx uninstall untether
```

## 3. Remove configuration and state

Untether stores all config and state in `~/.untether/` (or the path set by `UNTETHER_CONFIG_PATH`):

```sh
rm -rf ~/.untether/
```

This deletes:

| File | Contains |
|------|----------|
| `untether.toml` | Bot token, chat ID, engine settings, transport config |
| `*_state.json` | Chat preferences, session resume tokens, topic bindings |
| `active_progress.json` | Orphan message references (restart recovery) |
| `stats.json` | Per-engine run counts and usage statistics |

!!! warning "Bot token"
`untether.toml` contains your Telegram bot token in plaintext. Deleting the file removes it from disk.

## 4. (Optional) Delete the Telegram bot

Removing Untether does not delete the Telegram bot itself. If you no longer need it:

1. Open Telegram and message [@BotFather](https://t.me/BotFather)
2. Send `/deletebot`
3. Select your bot from the list

## 5. (Optional) Remove agent CLIs

If you no longer need the agent CLIs that Untether wrapped:

```sh
npm uninstall -g @anthropic-ai/claude-code
npm uninstall -g @openai/codex
npm uninstall -g opencode-ai
npm uninstall -g @mariozechner/pi-coding-agent
npm uninstall -g @google/gemini-cli
npm uninstall -g @sourcegraph/amp
```
43 changes: 43 additions & 0 deletions docs/how-to/update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Update Untether

Untether publishes releases to [PyPI](https://pypi.org/project/untether/). To upgrade to the latest version:

=== "uv (recommended)"

```sh
uv tool upgrade untether
```

=== "pipx"

```sh
pipx upgrade untether
```

Check your current version:

```sh
untether --version
```

After upgrading, restart the service if running as a systemd unit:

```sh
systemctl --user restart untether
```

!!! note "Agent CLIs are separate"
Untether wraps agent CLIs (Claude Code, Codex, OpenCode, Pi, Gemini CLI, Amp) as subprocesses. Updating Untether does not update the agent CLIs. Update them separately:

```sh
npm update -g @anthropic-ai/claude-code
npm update -g @openai/codex
npm update -g opencode-ai
npm update -g @mariozechner/pi-coding-agent
npm update -g @google/gemini-cli
npm update -g @sourcegraph/amp
```

## Checking for updates

Visit the [PyPI page](https://pypi.org/project/untether/) or the [changelog](https://github.com/littlebearapps/untether/blob/master/CHANGELOG.md) to see what's new.
2 changes: 1 addition & 1 deletion docs/reference/commands-and-directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ This line is parsed from replies and takes precedence over new directives. For b
| `/usage` | Show Claude Code subscription usage (5h window, weekly, per-model). Claude Code only. Requires Claude Code OAuth credentials (see [troubleshooting](../how-to/troubleshooting.md#claude-code-credentials)). |
| `/export` | Export last session transcript as Markdown or JSON. |
| `/browse` | Browse project files with inline keyboard navigation. |
| `/ping` | Health check — replies with uptime. |
| `/ping` | Health check — replies with uptime since last (re)start. Shows trigger summary if triggers target the current chat. |
| `/restart` | Gracefully drain active runs and restart Untether. |
| `/verbose` | Toggle verbose progress mode (on/off/clear). Shows tool details in progress messages. |
| `/config` | Interactive settings menu — plan mode, ask mode, verbose, engine, model, reasoning, trigger toggles with inline buttons. |
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ Quick definitions for terms used throughout the Untether documentation.

**Trigger**
: A webhook or cron rule that starts a run without a Telegram message. Triggers let external systems (GitHub, CI, schedulers) send tasks to Untether.

**Hot-reload**
: Applying configuration changes without restarting Untether. Requires `watch_config = true`. Hot-reloadable settings include trigger crons/webhooks, voice transcription, file transfer, and `allowed_user_ids`. Structural settings like `bot_token`, `chat_id`, and `session_mode` require a restart.

## Scheduling & triggers

**Delayed run**
: A one-shot run scheduled via `/at <duration> <prompt>`. The prompt executes after the specified delay (60 seconds to 24 hours). Pending delays are held in memory and lost on restart. Per-chat cap of 20.

**Webhook action**
: A lightweight action a webhook performs without spawning an agent run. Available actions: `file_write` (save POST body to disk), `http_forward` (relay payload to another URL), and `notify_only` (send a Telegram message). The default action (`agent_run`) starts a full agent session.
2 changes: 2 additions & 0 deletions docs/reference/runners/amp/runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Flags:
* `--stream-json` — JSONL output
* `--stream-json-input` — optional; enables stdin streaming (preliminary support, configurable)

Prompts starting with `-` are space-prefixed via `sanitize_prompt()` (base runner method) to prevent the CLI from interpreting the prompt as a flag.

For resumed sessions:

```text
Expand Down
10 changes: 10 additions & 0 deletions docs/tutorials/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,16 @@ This config file controls all of Untether's behavior. You can edit it directly o

[Full config reference →](../reference/config.md)

## Updating and uninstalling

To update Untether to the latest version:

```sh
uv tool upgrade untether
```

To uninstall completely (CLI, config, and state), see the [uninstall guide](../how-to/uninstall.md). To learn more about updates, see [Update Untether](../how-to/update.md).

## Re-running onboarding

If you ever need to reconfigure:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "untether"
authors = [{name = "Little Bear Apps", email = "hello@littlebearapps.com"}]
maintainers = [{name = "Little Bear Apps", email = "hello@littlebearapps.com"}]
version = "0.35.1rc5"
version = "0.35.1rc6"
keywords = ["telegram", "claude-code", "codex", "opencode", "pi", "gemini-cli", "amp", "ai-agents", "coding-assistant", "remote-control", "cli-bridge"]
description = "Run AI coding agents from your phone. Bridges Claude Code, Codex, OpenCode, Pi, Gemini CLI, and Amp to Telegram with interactive permissions, voice input, cost tracking, and live progress."
readme = {file = "README.md", content-type = "text/markdown"}
Expand Down
10 changes: 7 additions & 3 deletions scripts/healthcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ EXPECTED_VERSION=""
CHECKS_PASSED=0
CHECKS_FAILED=0

pass() { echo "OK: $1"; ((CHECKS_PASSED++)); }
fail() { echo "FAIL: $1"; ((CHECKS_FAILED++)); }
# Use explicit assignment (not `((var++))`) — post-increment returns the
# old value, which is 0 on first call and trips `set -e`.
pass() { echo "OK: $1"; CHECKS_PASSED=$((CHECKS_PASSED + 1)); }
fail() { echo "FAIL: $1"; CHECKS_FAILED=$((CHECKS_FAILED + 1)); }

# Parse arguments
while [[ $# -gt 0 ]]; do
Expand Down Expand Up @@ -73,7 +75,9 @@ if [[ -n "$EXPECTED_VERSION" ]]; then
fi

# 4. Recent errors (last 60 seconds)
ERROR_COUNT=$(journalctl --user -u "$SERVICE" -S "-60s" --no-pager -p err 2>/dev/null | grep -c . || true)
# `grep -v '^-- '` drops journalctl meta lines like "-- No entries --";
# `|| true` keeps the pipeline's exit 1 (no matches) from tripping set -e.
ERROR_COUNT=$(journalctl --user -u "$SERVICE" -S "-60s" --no-pager -p err 2>/dev/null | grep -vc '^-- ' || true)
if [[ "$ERROR_COUNT" -eq 0 ]]; then
pass "no ERROR-level log entries in last 60s"
else
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ nav = [
{ "Worktrees" = "how-to/worktrees.md" },
{ "Route by chat" = "how-to/route-by-chat.md" },
{ "Topics" = "how-to/topics.md" },
{ "Update" = "how-to/update.md" },
{ "Uninstall" = "how-to/uninstall.md" },
{ "Choose a mode" = "how-to/choose-a-mode.md" },
{ "Chat sessions" = "how-to/chat-sessions.md" },
{ "Context binding" = "how-to/context-binding.md" },
Expand Down
Loading