Skip to main content
How-To Guides Last updated: 11 March 2026

Dev setup

Set up Untether for local development, run checks, and test changes safely via the dev instance before releasing to production.

Set up Untether for local development, run checks, and test changes safely via the dev instance before releasing to production.

Clone and install

git clone https://github.com/littlebearapps/untether
cd untether

# Run directly with uv (installs deps automatically)
uv run untether --help

Install as a local tool (optional)

uv tool install .
untether --help

Two-instance model

systemd is optional The two-instance systemd model below is a Linux power-user setup. On any platform (including Linux), you can just run untether in a terminal — Ctrl+C and re-run to pick up changes.

Untether runs two separate instances on the same machine:

ProductionDev
Serviceuntether.serviceuntether-dev.service
Bot@your_production_bot@your_dev_bot
SourcePyPI wheel (frozen)Local editable (src/)
Config~/.untether/untether.toml~/.untether-dev/untether.toml
Binary~/.local/bin/untether (pipx).venv/bin/untether (editable)

Never restart production to test local changes systemctl --user restart untether does NOT pick up local code changes — production runs a frozen PyPI wheel. Restarting production during development is always wrong and risks disrupting live chat.

Development cycle

The standard workflow:

# 1. Edit source code
vim src/untether/telegram/commands/my_feature.py

# 2. Run checks
uv run pytest && uv run ruff check src/

# 3. Restart to pick up changes
uv run untether                      # Ctrl+C first if already running
# Or on Linux with systemd:
# systemctl --user restart untether-dev
# journalctl --user -u untether-dev -f

# 4. Test via @your_dev_bot in Telegram
$ journalctl --user -u untether-dev -f
Mar 10 09:15:23 lba-1 untether[12345]: untether.started version=0.34.0 engine=codex projects=3
Mar 10 09:15:23 lba-1 untether[12345]: telegram.connected bot=@untether_dev_bot
Mar 10 09:15:23 lba-1 untether[12345]: telegram.polling started

Always test via the dev bot before merging. Never send test messages to the production bot.

Run checks

# Individual checks
uv run pytest                        # tests (Python 3.12+, 80% coverage threshold)
uv run ruff check src tests          # linting
uv run ruff format --check src tests # formatting
uv run ty check .                    # type checking (warnings only, not blocking)

# All at once
just check

Format before committing Always run uv run ruff format src/ tests/ before committing — CI checks formatting strictly.

CI pipeline

GitHub Actions runs these checks on every push and PR:

JobWhat it checks
formatruff format --check --diff
ruffruff check with GitHub annotations
tyType checking (warnings only — 11 pre-existing warnings)
pytestTests on Python 3.12, 3.13, 3.14 with 80% coverage
builduv build wheel + sdist validation
lockfileuv lock --check ensures lockfile is in sync
install-testClean wheel install + import smoke-test (catches undeclared deps)
pip-auditDependency vulnerability scanning
banditPython security static analysis
docsDocumentation site build

Test conventions

  • Framework: pytest + anyio for async tests
  • Coverage: 80% threshold enforced in pyproject.toml
  • Patterns: Stub subprocess runners with fake CLI scripts, mock transport with FakeTransport dataclass
  • Key test files: test_claude_control.py (56 tests), test_callback_dispatch.py (28 tests), test_cost_tracker.py (56 tests)

Run specific test files:

uv run pytest tests/test_claude_control.py -x    # stop on first failure
uv run pytest tests/test_export_command.py -v     # verbose output
uv run pytest -k "test_approve"                   # run tests matching pattern

Promoting to production

Only after code is merged and released to PyPI:

# Upgrade the package
uv tool upgrade untether       # or: pipx upgrade untether

# Restart to apply:
/restart                           # from Telegram (preferred — drains active runs)
# Or from terminal: Ctrl+C first if running, then: untether
# Or on Linux with systemd: systemctl --user restart untether

Graceful restart Sending /restart in Telegram lets active runs finish before the service exits. This avoids interrupting in-progress tasks.

Branch naming

Follow conventional branch names:

  • feature/* — new features
  • fix/* — bug fixes
  • docs/* — documentation changes
Was this helpful?

Related Articles