awai
Run AI coding agents in tmux, then watch and drive them remotely through one small local API. The name comes from away + AI: you step away from the computer, the agent keeps working.
Telegram adapter ─┐
Web (planned) ─┼─ HTTP API + event stream (SSE) ──▶ awai core ──▶ tmux ──▶ claude
awai CLI ─┘ ◀── reads the transcript
Design principles
- Logic lives in one place. Everything a session does is in the core (
SessionManager). Telegram, the web and the CLI only call the public API; none of them touch tmux. - Code you can read in one sitting. The core uses only the Python standard library; there are no third-party runtime dependencies.
- Leaves your configuration alone. awai never writes
~/.claude/settings.json. It starts claude with--session-idto fix the session ID and--settingsto pass hooks that belong to that one session. - Only touches its own tmux. awai runs a separate tmux server (
tmux -L awai), so it can neither see nor change your other tmux sessions.
Security model
- The API listens on
127.0.0.1:8787by default. Binding to anything else requires an explicit[server] allow_remote = true, and you should put TLS in front of it. - Every request needs
Authorization: Bearer <token>. The token is random and stored in~/.awai/token(0600). - The
Hostheader is checked, so a malicious web page cannot reach the local service through DNS rebinding. Request bodies must beapplication/json, and no CORS headers are sent. - Configuration is read only from
~/.awai/config.toml; a.envin the current directory is never read. - The agent's argument list is handed to tmux directly, never through a shell. Text input may not contain control characters, and special keys go through an allow-list.
- Agents cannot be started in
/or in your home directory itself.[sessions] allowed_rootsnarrows this further. - The CLI client ignores proxy settings, so the token never goes through
HTTP_PROXY.
Install
Requires Python ≥ 3.11, tmux and Claude Code. Developed and tested on macOS; Linux should work but is untested.
uv tool install awai # or: pipx install awai
From source: git clone https://github.com/zeocax/awai && cd awai && uv run awai ….
Quick start
awai serve # start awai: the core plus the adapters enabled in the config (e.g. Telegram); creates the token on first run
# in another terminal
awai new ~/code/myapp # start claude in the project directory; prints the session ID
awai ls
awai send 3f2a "look at the failing tests" # a prefix of the session ID is enough
awai events # watch events live (JSON lines)
awai screen 3f2a # show the terminal screen
awai keys 3f2a Down Enter # answer prompts such as menus
awai attach 3f2a # take over at the computer; Ctrl-b d to detach
awai restart 3f2a # restart claude and continue the conversation (with the current hooks)
awai restart 3f2a --yolo # same, and switch to YOLO; --default switches back, --mode manual etc.
awai kill 3f2a
The first time claude starts in a directory it asks whether you trust it, and the preselected answer is "No, exit". Pressing Enter makes claude quit. To trust the directory:
awai keys 3f2a Down Enter
Telegram
awai serve starts the Telegram adapter when the config enables it. Like the CLI, it only calls the awai API. Each session gets a topic in your private chat with the bot, no matter where the session was created.
-
Create a bot with
/newbotin @BotFather and note its token. Then open BotFather's Mini App (the Open button next to the message box, not the text menus of/mybots), pick your bot, and turn on Threaded Mode in its topic (Threads) settings. The Telegram adapter checks this setting on start and logs an error if it is off (the core keeps running). -
Look up your numeric user ID with @userinfobot.
-
Open your bot in Telegram and tap Start (or send
/start). -
Add this to
~/.awai/config.toml(seeconfig.example.tomlfor every option):[telegram] enabled = true bot_token = "123456:ABC..." user_id = 123456789
-
Run
awai serve. If the log saysadapters: telegram, the Telegram adapter is running too. It shares the process with the core but still talks to it only through the local API. If it crashes it is restarted; a configuration error (wrong token, Threaded Mode off) is logged once instead.
Usage: with topics on, the bot's private chat has no "main chat", only topics. Create a topic and send /new ~/code/your-project in it: claude starts in that directory, and the topic becomes its conversation (it is renamed after the project). For sessions created on the computer with awai new, the bot creates a topic itself.
| Where | Send | Effect |
|---|---|---|
| A topic without a session | /new ~/code/app |
Start claude in that directory; the topic is now its conversation |
| A topic without a session | /yolo ~/code/app |
Same, but in YOLO mode; the topic name starts with 🎲 |
| A session topic | plain text | Typed into claude and submitted; claude's own slash commands such as /compact are passed through as is |
| A session topic | /screen |
Show the terminal screen, with Refresh / Esc / Enter / ⇧Tab / 1 2 3 / ↑ ↓ keys below it |
| A session topic | /keys Down Enter |
Press any keys from the allow-list |
| A session topic | /restart |
Restart claude and continue the conversation; brings older sessions up to date (hooks) |
| A session topic | /restart yolo |
Same, and switch to YOLO mode (the topic name gets 🎲); /restart default switches back |
| A session topic | /kill |
End the session |
| Any topic | /delete |
Delete this topic (after a confirmation; its session is ended first). You cannot delete topics the bot created from the Telegram client, so this lets the bot do it |
| Any topic | /ls, /help |
List sessions, show help |
Typing / opens the command menu.
Claude's replies are rendered as Markdown: code blocks, inline code, bold, italic, links, lists and quotes become Telegram formatting. Telegram has no tables, so tables become column-aligned monospace text. The conversion uses Telegram entities rather than parse_mode, so escaping can never make a message fail; if Telegram still rejects the formatting, the message is sent again as plain text.
Tool calls arrive as silent messages with a "📺 Screen" button. When Claude stops to wait for a permission answer, tap it to see the screen and answer with the keys below it. Text typed on the computer is shown with a leading 🖥️.
Security: the bot responds only to user_id, and only in that user's private chat with the bot. Other people, groups, channels and forwarded messages are ignored without a reply. Requests to Telegram use HTTPS_PROXY if you need a proxy; requests to the local awai API never go through a proxy.
Remote approvals and questions
When Claude asks for permission (to run a command, edit a file, …), Telegram shows:
🔐 Claude wants to use Bash
rm -rf build
(Remove build directory)
[✅ Allow] [❌ Deny]
When Claude asks a question (say, "Which file should I create?"), each question arrives as its own message with the options as buttons. For multiple-choice questions, tick the options and tap "✅ Done"; you can also answer by replying with text. On the command line, awai pending lists open prompts, and awai allow|deny <ID> and awai answer <ID> answer… answer them.
The terminal still shows these menus and questions, so you can answer at the computer too; whichever answers first wins. awai learns about questions and options through hooks that apply only to the sessions it starts (claude --settings; your ~/.claude/settings.json is left alone). The hooks return immediately and never decide anything for you. A remote answer is translated into key presses for the menu, and awai first checks that the menu really is on screen. Deny is the same as pressing Esc in the terminal: Claude stops and waits for you to say what to do next. Details in docs/api.md.
If Claude is in auto mode (the status bar shows auto mode on), Claude decides most actions on its own and rarely asks. To approve every action yourself, switch to the normal mode with ⇧Tab, or start with --mode manual.
YOLO mode
awai new ~/code/app --yolo # CLI
/yolo ~/code/app # Telegram
A running session can switch too: /restart yolo (or awai restart <ID> --yolo) restarts it in YOLO mode and continues the conversation; /restart default switches back.
This starts Claude with --dangerously-skip-permissions: running commands and changing files no longer ask for confirmation, so no remote approvals appear either. The mode is chosen per session; in Telegram such topics start with 🎲, and a warning is posted on start. In the API, the session's mode is "yolo".
It gives nobody who can drive awai any new power: whoever can use the Telegram bot could already tap "Allow", or run shell commands directly with !. The real new risk is prompt injection: files in the repository or web content can talk Claude into running commands, and in YOLO mode nobody sees a confirmation. Use it only when you trust everything in the directory.
--mode manual|auto|acceptEdits|plan picks other permission modes; see docs/api.md.
Configuration
The configuration file is ~/.awai/config.toml (set AWAI_HOME to use another directory than ~/.awai). It is split into one section per component, and every section and key is optional. config.example.toml in the repository lists every option with its default:
| Section | Keys | Purpose |
|---|---|---|
[server] |
host, port, allow_remote, allowed_hosts |
The local HTTP API |
[sessions] |
allowed_roots |
Directories sessions may start in (empty: anywhere except / and your home directory itself) |
[claude] |
command, config_dir |
How to start claude (an argument list; no permission flags) and where its transcripts are |
[tmux] |
socket, session |
awai's own tmux server |
[telegram] |
enabled, bot_token, user_id |
If this section exists (and does not say enabled = false), awai serve starts the Telegram adapter |
The old flat layout (e.g. a top-level port = 8787) is rejected with an error that says which section the key moved to.
API
See docs/api.md.
Development
uv run pytest -q
tests/test_tmux.py and tests/test_e2e.py start throwaway tmux servers (removed when the tests finish). The end-to-end tests use tests/fake_agent.py instead of claude, so they need no account and spend no tokens.
Roadmap
- Telegram adapter (long polling, public API only)
- Web client (served same-origin by the core)
- Remote permission approvals (session-scoped hooks)
- Remote answers to questions (AskUserQuestion)
- Remote plan approval (ExitPlanMode)
- Status detection: working, idle
- Other agents (Codex, Gemini, …)
Acknowledgements
awai was inspired by ccgram, which drives Claude Code in tmux from Telegram topics. awai is a separate, much smaller implementation written from scratch.
Metadata
Release files for awai 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| awai-0.1.1.tar.gz | 83.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| awai-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 141.7 kB
Release files / awai-0.1.1.tar.gz
| Download URL | awai-0.1.1.tar.gz |
|---|---|
| Size | 83.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aa8638a220f582df2c77d50f99c98369ce17e3fe660964b1322969de5c83d0e5
|
|
BLAKE2b-256 checksum How to use checksums |
9114d176bfe288499d46ab66deb16c6ae26e85b0d84658d8a891dea8f576c9f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.12
|
Release files / awai-0.1.1-py3-none-any.whl
| Download URL | awai-0.1.1-py3-none-any.whl |
|---|---|
| Size | 58.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2e82b4dd9c4f4d7bbca096a0fc43a38727b22c49eddc5619d2f3f4a75868fc9f
|
|
BLAKE2b-256 checksum How to use checksums |
f3d6b7769fd1f98a8ab8ea10fa4817f79349f0518d845f6702a8e16eee3f7ec6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.12
|