Skip to main content
How-To Guides Last updated: 20 April 2026

Operations and monitoring

Untether runs as a long-lived process, typically in a terminal or managed by a process supervisor (systemd on Linux, etc.). This guide covers health checks, ...

Untether runs as a long-lived process, typically in a terminal or managed by a process supervisor (systemd on Linux, etc.). This guide covers health checks, graceful restarts, diagnostics, and day-to-day operations — all controllable from Telegram without SSH.

Health check

Send /ping in Telegram to verify the bot is running:

Untether pong — up 3d 14h 22m

The response includes the bot’s uptime since last restart. Use this as a quick liveness check.

If triggers (crons or webhooks) target the current chat, /ping also shows a trigger summary:

Untether pong — up 3d 14h 22m ⏰ triggers: 1 cron (daily-review, 9:00 AM daily (Melbourne)), 1 webhook

If webhooks and cron are enabled, the webhook server also exposes a health endpoint:

GET http://127.0.0.1:9876/health

Returns {"status": "ok", "webhooks": N} where N is the number of configured webhooks. Useful for external monitoring tools.

Health snapshot

/health consolidates RAM, the Untether process, triggers, and today’s API cost into a single message — handy as a one-shot diagnostic when a chat suddenly stops responding.

Untether 💚 health 🧠 RAM: 18.2 / 32.0 GB · swap: 0 / 4.0 GB 🐍 untether: pid 1543657 · 70 MB RSS · 13 FDs · 1 child ⏰ triggers: 2 crons, 1 webhook 💰 today: $1.42 ⏱ uptime: 3d 14h 22m

Each section degrades gracefully when its source is unavailable (non-Linux, no trigger_manager, no cost tracker). /health is project-aware — children reflects the current Untether process tree (Claude Code subprocesses, MCP servers, workerd grandchildren under #275-style cleanup). When triggers are disabled in config, the line reads triggers: disabled.

RAM guard (#350)

Untether refuses to spawn a new engine subprocess when free RAM is below [watchdog] prespawn_ram_block_mb (default 500 MB), and warns at prespawn_ram_warn_mb (default 2000 MB). On block the run completes early with 🛑 Insufficient RAM instead of spawning a doomed subprocess that would leak memory under OOM. Set either threshold to 0 to disable that tier; 0 / 0 disables the guard entirely. See config: [watchdog].

Graceful restart

Send /restart in Telegram to initiate a graceful shutdown:

  1. Untether stops accepting new runs
  2. Active runs are drained (allowed to finish)
  3. The process exits cleanly
  4. Run untether again in your terminal (or your process supervisor restarts it automatically)

Prefer /restart over killing the process /restart lets in-progress runs complete before shutting down. Killing the process with kill or systemctl restart may interrupt active runs and lose work.

SIGTERM behaviour

Sending SIGTERM to the Untether process triggers the same graceful drain as /restart:

  1. New runs are rejected
  2. Active runs are allowed to complete
  3. After a 120-second drain timeout, remaining runs are cancelled and the process exits

This means systemctl --user stop untether (Linux) also drains gracefully, as systemd sends SIGTERM first. Pressing Ctrl+C in a terminal sends SIGINT, which triggers the same graceful drain.

Message continuity across restarts

Untether persists the last Telegram update_id to last_update_id.json in the config directory. On startup, polling resumes from the saved offset — no messages are dropped or re-processed within Telegram’s 24-hour retention window. Pending /at delays are cancelled during drain and not persisted (they are lost on restart).

Drain timeout The default drain timeout is 120 seconds. If active runs don’t complete within this window, they are cancelled and a timeout notification is sent to Telegram.

Orphan progress cleanup

When Untether restarts (after a crash, upgrade, or manual restart), any progress messages from the previous instance are still visible in Telegram — stuck showing “working” with stale elapsed time.

Untether automatically handles this: active progress messages are tracked in active_progress.json in the config directory. On startup, any orphan messages from a prior instance are edited to show:

Untether ⚠️ interrupted by restart

This replaces the stale progress text and removes any inline keyboards (approval buttons), so there’s no confusion about which messages are from the current session.

The cleanup happens before the startup message is sent, so by the time you see “Untether started”, all orphan messages are already resolved.

Systemd service (Linux)

The recommended systemd unit file is provided at contrib/untether.service. Key settings:

SettingValuePurpose
Type=notifyUntether sends READY=1 after startup completes; systemd knows the service is ready
NotifyAccess=mainOnly the main process can send sd_notify signals
RestartSec=2Wait 2 seconds before auto-restarting on failure
OOMScoreAdjust=-100Makes Untether less likely to be OOM-killed than default processes
OOMPolicy=continueDon’t stop the service if a child process is OOM-killed
KillMode=mixedSends SIGTERM to main process, SIGKILL to remaining children after timeout

Copy the unit file and reload:

cp contrib/untether.service ~/.config/systemd/user/untether.service
systemctl --user daemon-reload
systemctl --user enable --now untether

See the dev instance reference for full service file documentation.

Auto-continue (Claude Code)

When Claude Code exits after receiving tool results without processing them (an upstream bug), Untether detects the premature exit and automatically resumes the session. You’ll see a “⚠️ Auto-continuing” notification in the chat.

Auto-continue is enabled by default. It is suppressed for signal deaths (SIGTERM, SIGKILL) to prevent death spirals under memory pressure.

Configure via [auto_continue] in untether.toml:

KeyDefaultNotes
enabledtrueEnable automatic session resumption.
max_retries1Maximum consecutive retries per run (1–5).

See troubleshooting for details on when this triggers and how to tune it.

Run diagnostics

Run the built-in preflight check to validate your configuration:

untether doctor

This validates:

  • Telegram bot token is valid and the bot is reachable
  • Chat ID is correct and the bot can send messages
  • Topics configuration (if enabled)
  • File transfer permissions and deny globs
  • Voice transcription setup
  • Engine availability (Claude Code, Codex, OpenCode, Pi, Gemini CLI, Amp)

Run this after any config change, after upgrading, or when something isn’t working.

Debug mode

Start Untether with debug logging to troubleshoot issues:

untether --debug

This logs detailed information to debug.log, including:

  • Engine JSONL events (every line from the subprocess)
  • Telegram API requests and responses
  • Rendered messages and inline keyboards
  • Config loading and validation

Check debug.log first When reporting issues, include the relevant section of debug.log. It contains everything needed to diagnose most problems.

Config hot-reload

Enable config watching so Untether picks up changes without a restart:

=== “untether config”

```sh
untether config set watch_config true
```

=== “toml”

```toml title="~/.untether/untether.toml"
watch_config = true
```

When enabled, Untether watches the config file for changes and reloads most settings automatically.

Hot-reloadable (applied immediately):

  • Trigger system: triggers.enabled, crons, webhooks, auth, rate limits, timezones
  • Telegram bridge: voice_transcription, [files], allowed_user_ids, show_resume_line, timing
  • Engine defaults, budget, cost/usage display flags

Restart-only (require /restart or systemctl restart):

  • bot_token, chat_id (Telegram connectivity)
  • session_mode, topics.enabled (structural)
  • message_overflow (message splitting strategy)

Process management

=== “Telegram (all platforms)”

Send `/restart` in Telegram for a graceful restart with drain visibility.
Use `/ping` to check the bot is running.

=== “Terminal (all platforms)”

Stop with Ctrl+C (if running), then:

```sh
untether
```

View output directly in the terminal. Use `--debug` for verbose logging to `debug.log`.

=== “Linux (systemd)”

```bash
systemctl --user restart untether
journalctl --user -u untether -f       # live logs
systemctl --user status untether       # check status
journalctl --user -u untether -n 100   # recent logs
```

Restart vs /restart systemctl --user restart untether sends SIGTERM, which triggers a graceful drain. However, /restart in Telegram gives you a confirmation message and visibility into the drain process. Prefer /restart when you have Telegram access — it works on all platforms.

Was this helpful?

Related Articles