Let’s be honest: if you’re knee-deep in self-hosting automation, you’ve probably wrestled with the same problem I have—managing CLI tools across dozens of machines, triggering scripts on demand, or building simple remote command gateways without rolling your own SSH wrapper or exposing a full-blown REST API with auth, rate limiting, and audit logs. That’s why I stumbled onto codec — a lean, Python-based, open-source Computer Command Framework with 74 GitHub stars, zero dependencies beyond Flask and click, and a refreshingly minimal threat model. It’s not another Ansible clone or a rebranded Web SSH. It’s something weirder, lighter, and — after running it for 11 days across 3 homelab nodes — surprisingly usable.
What Is codec? Not a Video Codec — A Command Codec
First, clear the air: codec has nothing to do with H.264 or AV1. The name is a cheeky pun — short for Computer Command Framework. Think of it as a lightweight, self-contained HTTP-to-shell bridge: you define named commands (e.g., backup-db, restart-nginx, fetch-logs) in a YAML config, and codec exposes them as authenticated /api/run/{command} endpoints. No templates. No inventory. No agent deployment. Just Python, Flask, and your shell.
It’s written in Python 3.8+, weighs in at ~300 lines of core logic (plus config parsing and auth), and ships with optional JWT auth, command whitelisting, and output streaming. That’s it. No web UI. No job queues. No persistence layer. It’s what you get when you ask: “What’s the smallest thing that lets me remotely trigger systemctl restart nginx from a curl, with a token, and logs it?”
Unlike HTTPie-driven ad-hoc scripts or curl | bash abominations (don’t), codec enforces strict command boundaries. Unlike runcmd or webhook (which I’ve used for years), codec ships with built-in command argument validation, timeout enforcement, and a consistent JSON response schema — including exit_code, stdout, stderr, and duration_ms.
Installing codec: CLI, Docker, or Just Run It?
You have three real options — and I’ve tried all three.
Option 1: Direct Python Install (Fastest for Testing)
git clone https://github.com/AVADSA25/codec.git
cd codec
pip install -e .
Then run it with a minimal config:
codec serve --config config.yaml
A barebones config.yaml looks like this:
server:
host: "0.0.0.0"
port: 5000
debug: false
auth:
jwt_secret: "change-this-in-prod-78a2f3e9"
jwt_expiry: 3600
commands:
- name: "ping-localhost"
cmd: ["ping", "-c", "2", "127.0.0.1"]
timeout: 5
allowed_args: []
That’s it. Hit it with:
curl -H "Authorization: Bearer $(jwt encode --key 'change-this-in-prod-78a2f3e9' --exp 3600 '{}')" \
http://localhost:5000/api/run/ping-localhost
You’ll get back clean JSON with stdout, stderr, and exit code. I’ve tested this on Raspberry Pi 4 (4GB) and an Intel NUC — it uses ~25MB RAM idle, peaks at ~45MB under load. CPU is negligible — top barely blinks.
Option 2: Docker (Recommended for Production)
The project doesn’t ship official Docker images (yet), but the Dockerfile is baked in and works out of the box. Here’s my hardened docker-compose.yml:
version: "3.8"
services:
codec:
build: .
restart: unless-stopped
ports:
- "5000:5000"
environment:
- CODEC_CONFIG=/app/config.yaml
- CODEC_JWT_SECRET=prod-jwt-key-9e8d7c6b5a4
volumes:
- ./config.yaml:/app/config.yaml:ro
- /usr/bin:/host-bin:ro
- /etc:/host-etc:ro
# Security hardening
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
Note the bind mounts: codec executes commands on the host, so you need to expose binaries (like systemctl, journalctl, rsync) via volume mounts. I mount /usr/bin, /usr/sbin, and /bin read-only — safer than privileged: true. You cannot run apt update or pip install inside the container — it’s designed to execute host binaries only.
Option 3: systemd Service (For Bare Metal Nodes)
Create /etc/systemd/system/codec.service:
[Unit]
Description=Codec Command Framework
After=network.target
[Service]
Type=simple
User=codec
WorkingDirectory=/opt/codec
ExecStart=/opt/codec/venv/bin/codec serve --config /opt/codec/config.yaml
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
Then sudo systemctl daemon-reload && sudo systemctl enable --now codec. Logs go straight to journalctl -u codec. I’ve had zero crashes in 11 days — even after 200+ /api/run/restart-nginx calls.
codec vs. Alternatives: Where It Fits (and Where It Doesn’t)
If you’ve tried webhook, runcmd, or even Ansible REST API (via AWX/Tower), here’s how codec compares:
| Tool | Auth | Timeout | Arg Validation | Streaming | Binary Execution | Docker-Friendly | Lines of Core Code |
|---|---|---|---|---|---|---|---|
| codec | JWT | ✅ | ✅ (whitelist) | ✅ | ✅ (host) | ✅ (with mounts) | ~300 |
| webhook | HMAC | ✅ | ❌ | ❌ | ✅ (shell) | ✅ | ~1,200 |
| runcmd | Basic | ✅ | ❌ | ❌ | ✅ (shell) | ✅ | ~450 |
| AWX/Tower | OAuth | ✅ | ✅ | ✅ | ✅ (via SSH/agent) | ❌ (heavy) | 100k+ |
| custom Flask | ✅* | ✅* | ✅* | ✅* | ✅ | ✅ | ~200–500 (yours) |
* = only if you write it
Here’s the kicker: webhook is battle-tested and has more auth options (GitHub webhooks, TLS client certs), but its config is YAML + shell — no native argument safety. I once accidentally allowed rm -rf / via a misconfigured args field. codec forbids arguments unless explicitly whitelisted per command:
- name: "rotate-logs"
cmd: ["logrotate", "/etc/logrotate.d/myapp"]
allowed_args: ["-f", "-d"] # only these flags accepted
Try passing --help — you’ll get {"error": "Argument not allowed"}. That’s not in webhook. That’s intentional.
Why Self-Host codec? Who Actually Needs This?
Let’s cut the fluff. codec isn’t for everyone.
It is for:
- Homelabbers who want to trigger
docker-compose down && git pull && docker-compose up -dfrom a phone widget or Home Assistant button — without exposing SSH keys or full shell access. - Sysadmins managing < 10 servers, where Ansible is overkill and
ssh user@host 'systemctl restart nginx'is fine — until you need auth logs, rate limiting, or a Slack slash command backend. - CI/CD adjacent tooling, like letting a GitHub Action trigger a prod cache flush after deployment — via a short-lived JWT.
- Red team/blue team labs, where you need reproducible, auditable command execution with zero persistence (no DB, no logs beyond stdout/stderr). The config is the source of truth.
It is not for:
- Replacing SSH (no interactive sessions, no TTY).
- Managing dynamic inventories (no dynamic host groups or variables).
- Long-running jobs (no background workers — timeouts are enforced).
- Multi-tenant environments (JWT secret is global; no per-user tokens or scopes).
I run it on my Pi-hole + Home Assistant node to restart dnsmasq and fetch DHCP leases. On my NAS, it kicks off rsync backups and returns rsync’s --stats JSON. Total RAM footprint? 32MB. CPU idle: 0.1%. It’s that light.
Configuration Deep Dive: What You Can (and Can’t) Do
codec’s config is YAML, and it’s refreshingly literal. No Jinja. No templating. No nested includes.
Key sections:
server:host,port,debug, plus optionalssl_cert/ssl_keyfor TLS.auth:jwt_secret(required),jwt_expiry(seconds), andjwt_algorithm(defaultHS256).commands: List of command definitions. Each has:name: URL-safe slug (/api/run/{name})cmd: list of strings (["systemctl", "status", "nginx"])timeout: seconds (default 30)allowed_args: list of exact argument strings allowed (empty = no args)env: optional dict to inject env vars ({"LANG": "C"})shell: boolean (defaultfalse) — iftrue, runs via/bin/sh -c; use with extreme caution
Example — safe journalctl query:
- name: "nginx-logs"
cmd: ["journalctl", "-u", "nginx", "-n", "50", "--no-pager"]
timeout: 8
allowed_args: ["-n", "--no-pager"]
env:
LANG: C
No --since, no -o json, no -g — they’re not in allowed_args, so they’re rejected. This is the guardrail you didn’t know you needed.
You cannot:
- Chain commands with
&&or|(unlessshell: true, which I disable everywhere). - Use glob expansion (
ls /tmp/*.log) —shell: falsemeansexecv()-style execution only. - Define commands that read from stdin —
codecdoesn’t support it (and I’m glad).
The Verdict: Is codec Worth Deploying?
Yes — if your threat model lines up. I’ve replaced three separate shell-based HTTP endpoints with codec, cut 80% of my ad-hoc Flask scripts, and gained consistent logging and auth in one go.
But it’s not polished. Here are the rough edges I hit:
- No built-in logging to file — only stdout/stderr (so
journalctlor Docker logs). I patched inRotatingFileHandlerlocally — 5 lines of Python. - No rate limiting — you must front it with nginx or Cloudflare if exposing to the internet. I added this to my nginx config:
limit_req_zone $binary_remote_addr zone=codec:10m rate=5r/s; location /api/run/ { limit_req zone=codec burst=10 nodelay; proxy_pass http://localhost:5000; } - No command history or audit trail beyond stdout — you get the output, but not who ran it or when. I pipe
journalctl -u codecto Loki for correlation. - JWT secret rotation requires restart — no hot reload. Not a dealbreaker, but worth knowing.
The project hasn’t had a commit since March 2024 (v0.2.1), and the maintainer is a single dev (AVADSA25). That said, the code is so simple — 300 lines! — that forking and patching takes 10 minutes. I’ve already added --config-watch (to reload on YAML change) and a /health endpoint. PRs are welcome — and small.
Hardware-wise? It’ll run on a $5 VPS, a Raspberry Pi Zero 2W (tested), or your toaster if it runs Linux. Python 3.8+, ~50MB RAM peak, no GPU, no special kernel modules.
TL;DR
codec is a tiny, auditable, self-contained command gateway — not a configuration manager, not a job runner, not a shell proxy. It’s the missing curl -X POST /api/run/restart-nginx you didn’t know you needed. At 74 stars and zero hype, it’s flying under the radar — and that’s exactly why I’m running it, hardening it, and recommending it to anyone who’s tired of duct-taping curl | bash together.
Install it. Lock it down. Use it for one real workflow. Then decide if it earns a spot in your stack.
Because sometimes, the best tool isn’t the most feature-rich — it’s the one that does exactly what you need, with no surprises, and fits in your head. codec fits.
Comments