https://github.com/zachlagden/discordctl
Control a Discord server from the command line — a localhost command bus and dctl CLI for full, dry-run-safe bot-driven server management.
https://github.com/zachlagden/discordctl
automation cli command-bus devops discord discord-bot discord-py python server-management uv
Last synced: about 12 hours ago
JSON representation
Control a Discord server from the command line — a localhost command bus and dctl CLI for full, dry-run-safe bot-driven server management.
- Host: GitHub
- URL: https://github.com/zachlagden/discordctl
- Owner: zachlagden
- License: mit
- Created: 2026-06-18T21:16:23.000Z (17 days ago)
- Default Branch: main
- Last Pushed: 2026-07-03T17:07:23.000Z (2 days ago)
- Last Synced: 2026-07-03T19:09:34.638Z (2 days ago)
- Topics: automation, cli, command-bus, devops, discord, discord-bot, discord-py, python, server-management, uv
- Language: Python
- Size: 702 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# discordctl
**Control a Discord server from the command line — full, bot-driven server management with dry-run-by-default safety.**
[](LICENCE)
[](https://github.com/zachlagden/discordctl/stargazers)
[](https://github.com/zachlagden/discordctl/commits/main)
[](https://github.com/zachlagden/discordctl/issues)




[Setup](docs/SETUP.md) · [Live smoke test](docs/SMOKE.md) · [Report Bug](https://github.com/zachlagden/discordctl/issues) · [Request Feature](https://github.com/zachlagden/discordctl/issues)
---
## What is discordctl?
`discordctl` turns a Discord bot into a scriptable command-line control plane for your server. A
persistent [discord.py](https://discordpy.readthedocs.io/) daemon holds the gateway connection and
exposes a **localhost-only HTTP command bus**; the `dctl` CLI drives that bus over the shell — so you
(or an automation, or an AI coding agent) can ban and edit members, manage roles, channels, categories,
permissions, messages, threads, emojis, invites, and webhooks. **179 operations** in all, covering the
whole management surface.
It was built to let an AI agent administer a server safely, so safety is the headline: **every mutating
operation is dry-run by default** and requires an explicit `--confirm`, writes are bounded by a guild
allowlist and a global kill switch, and every call is appended to an audit log.
---
## Features
| Feature | Description |
| --- | --- |
| **179 operations** | Guild, channel, category, role, member, message, permissions, thread, emoji, invite, and webhook management — driven as `dctl op `. |
| **Dry-run by default** | Every mutation previews what it *would* do and changes nothing until you pass `--confirm`. The client cannot bypass this. |
| **Guarded & auditable** | Guild allowlist, `WRITE_ENABLED` kill switch, owner-ban refusal, `--yes-really` for destructive ops, and an append-only JSONL audit log of every call. |
| **Declarative state** | `guild.snapshot` → `guild.diff` → `guild.apply`: capture the server to JSON, edit it, apply the difference. |
| **Localhost-only bus** | The control bus binds `127.0.0.1`, trusts only loopback peers, and authenticates every request with a constant-time bearer check. |
---
## Quick Start
### 1. Clone & install
```bash
git clone https://github.com/zachlagden/discordctl.git
cd discordctl
uv sync
```
### 2. Configure
```bash
cp .env.example .env
# Set DISCORD_TOKEN, BUS_TOKEN, ALLOWED_GUILD_IDS, DEFAULT_GUILD_ID
```
Generate the bot invite URL and authorize it into your server (see [docs/SETUP.md](docs/SETUP.md) for
the full walkthrough, including the required privileged intents):
```bash
uv run python scripts/invite_url.py
```
### 3. Run & drive it
```bash
uv run python -m discordctl # or: docker compose up -d
uv run dctl health
uv run dctl ops
uv run dctl op guild.info
uv run dctl op role.create --arg name=example # dry-run (preview only)
uv run dctl op role.create --arg name=example --confirm # executes
```
---
## How it works
```
you / a script / an agent
│ shell
▼
dctl ──HTTP POST /v1/op (bearer, 127.0.0.1)──► control bus (aiohttp)
▲ │
│ JSON ▼
└────────────────────────────────────── op registry → handler → discord.py
│
▼
Discord API
```
The daemon stays connected to the gateway (warm cache, bot shows online). `dctl` is stateless and talks
to the bus over loopback. For mutating ops the **server** decides the effective dry-run state from
`confirm` + `WRITE_ENABLED` — a crafted request body can't force a live write.
---
## Tech Stack
| Layer | Technology |
| --- | --- |
| **Gateway daemon** | Python 3.11+, discord.py 2.x |
| **Control bus** | aiohttp (localhost), pydantic v2 |
| **CLI** | `dctl` (stdlib argparse + urllib) |
| **Tests / quality** | pytest, pytest-asyncio, ruff |
| **Deploy / observability** | Docker, Docker Compose, Sentry |
---
## Project Structure
```
src/discordctl/
├── __main__.py # python -m discordctl -> runs the daemon
├── config.py # env-driven configuration
├── schemas.py # OpRequest / OpResponse
├── daemon/ # discord.py client + aiohttp control bus
├── ops/
│ ├── registry.py # @op decorator, BusContext, dry-run plan()
│ ├── lookup.py # entity resolution + guild allowlist
│ ├── serialize.py # discord objects -> JSON
│ ├── audit.py # append-only audit writer
│ └── handlers/ # one module per domain (179 ops)
└── cli/dctl.py # the CLI
docs/ # SETUP.md, SMOKE.md, design spec & plan
```
---
## Configuration
Loaded from `.env` (gitignored — never commit real values).
| Variable | Purpose |
| --- | --- |
| `DISCORD_TOKEN` | Bot token (all privileged intents required) |
| `BUS_TOKEN` | 256-bit hex bearer token for the control bus |
| `BUS_HOST` / `BUS_PORT` | Bus bind address (keep loopback) / port |
| `ALLOWED_GUILD_IDS` | Comma-separated guild allowlist (empty disables it) |
| `DEFAULT_GUILD_ID` | Guild used when an op omits `guild_id` |
| `WRITE_ENABLED` | Global kill switch; `false` forces every mutation to dry-run |
| `AUDIT_PATH` | Path to the append-only audit log |
| `SENTRY_DSN` | Optional Sentry DSN |
---
## Star History
---
## Support
If this project is useful to you, consider supporting development:
---
## License
This project is licensed under the MIT License — see the [LICENCE](LICENCE) file for details.
---
**[Report Bug](https://github.com/zachlagden/discordctl/issues)** · **[Request Feature](https://github.com/zachlagden/discordctl/issues)**
Made by [Zach Lagden](https://github.com/zachlagden)