█████ █████ █████████ █████████ █████ ████ ░░███ ░░███ ███░░░░░███ ███░░░░░███░░███ ███░ ░███ ░███ ░███ ░███ ███ ░░░ ░███ ███ ░███████████ ░███████████ ░███ ░███████ ░███░░░░░███ ░███░░░░░███ ░███ ░███░░███ ░███ ░███ ░███ ░███ ░░███ ███ ░███ ░░███ █████ █████ █████ █████ ░░█████████ █████ ░░████ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░ ░░░░
Opinionated local-dev orchestration for running multiple projects at the same time without port conflicts.
Network isolation per repo / branch: every instance runs on its own Docker network (so Postgres/Redis/etc can stay on default ports inside the project).
Stable HTTPS hostnames: https://<project>.hack (and subdomains like https://api.<project>.hack) routed by a global Caddy proxy.
Good logs UX: instant docker compose logs tailing, plus Loki/Grafana for querying + history.
Opt-in per repo: no invasive changes to your codebase; config lives in .hack/.
Most of my projects run the same stack. That’s fine until you want to:
- run two projects at the same time
- run two branches of the same repo
- run multiple worktrees in parallel
At that point everything fights over localhost and default ports.
The daily choice:
- Option A: stop A → start B → ship fix → stop B → restart A
you just spent 5–10 minutes paying the orchestration tax and nuking your focus. - Option B: don’t run it. code blind. push. let CI tell you what you broke.
Neither scales. Both slow you down in dumb, repeatable ways.
- macOS: supported.
- Docker + Compose: Orbstack | Docker Desktop
Option A: Shell script (CLI only)
curl -fsSL https://github.com/hack-dance/hack/releases/latest/download/hack-install.sh | bashOption B: macOS DMG (CLI + Desktop App)
Download the latest DMG from GitHub Releases.
The DMG includes:
- Hack Desktop - macOS menu bar app for managing projects
- Install Hack CLI - double-click to install the CLI to
~/.hack/bin
hack global installManual (CLI):
cd /path/to/your-repo
hack init
hack up --detach
hack openAgent-assisted (Cursor/Claude/Codex with shell access):
hack setup cursor # or hack setup claude / hack setup codex
hack setup agents # optional: adds AGENTS.md + CLAUDE.md snippets
hack agent init --client cursor # or --client claude / --client codex
hack agent patterns # optional: dependency/ops checklistIf you omit --client, hack agent init will prompt you to choose (TTY only).
If your agent does not auto-open, paste the output into the chat. Example:
I ran `hack agent init` and pasted the output below. Please follow it to set up hack for this repo,
then run:
- `hack init` (use --auto if dev scripts are detected)
- `hack up --detach`
- `hack open --json`
If anything fails, use `hack logs --pretty` and summarize next steps.
<PASTE HACK AGENT INIT OUTPUT HERE>
If the agent cannot run shell commands, use MCP instead: hack setup mcp and hack mcp serve.
hack includes a lightweight, git-backed ticket system for tracking work without leaving your repo. Tickets are stored in a hidden git ref (refs/hack/tickets) so they sync with your code but don't clutter your branch list.
Quick setup:
# Enable tickets for this repo
hack x tickets setup
# Create a ticket
hack x tickets create --title "Add dark mode support"
# List tickets
hack x tickets list
# Interactive TUI for managing tickets
hack x tickets tui
# Update status
hack x tickets status T-00001 in_progress
# Sync to remote
hack x tickets syncWhy use it:
- No external service required - tickets live in git
- Agent-friendly - Claude/Cursor/Codex can create and manage tickets
- Syncs with
git push/pullvia hidden refs - Works offline
Tickets are ideal for solo devs, small teams, or agent-driven workflows where you want lightweight task tracking without context-switching to Jira/Linear/GitHub Issues.
Full docs: docs/guides/tickets.md
Source lives in apps/macos. See apps/macos/README.md for XcodeGen and build/run steps.
Quick commands (repo root):
bun run macos:project-gen
bun run macos:open
bun run macos:dev
bun run macos:build
bun run macos:testname: project slug (also used for Docker Compose project name)dev_host: base hostname (<dev_host>.hack)
Optional [logs] settings
follow_backend:compose|loki(default:compose)snapshot_backend:compose|loki(default:loki)clear_on_down: when runninghack down, request Loki delete for this project (best-effort)retention_period: e.g."24h"; onhack down, prune older logs (best-effort)
Optional [internal] settings (container DNS/TLS)
dns: use CoreDNS to resolve*.hackinside containers (default:true)tls: mount Caddy Local CA + set common SSL env vars (default:true)extra_hosts: static Composeextra_hostsentries (hostname → IP/target), merged into the internal override
If you need dynamic extra_hosts (e.g. Pulumi outputs / local tunnels that change), use:
hack internal extra-hosts set <hostname> <target>
hack internal extra-hosts unset <hostname>
hack internal extra-hosts listThis writes .hack/.internal/extra-hosts.json and is merged into extra_hosts when you run hack up / hack restart.
Optional [oauth] settings (OAuth-safe alias host)
enabled: when true,hack initgenerates Caddy labels so routed services answer on both:- primary:
https://<dev_host> - OAuth alias:
https://<dev_host>.<tld>(e.g.https://sickemail.hack.gy) tld: optional (default:"gy"). Only*.hack.gyis bootstrapped automatically byhack global install; other TLDs require manual DNS setup.
The file includes a JSON Schema reference for editor validation:
{
"$schema": "https://schemas.hack/hack.config.schema.json"
}Schemas are served locally by the global Caddy proxy at https://schemas.hack.
Quick edits:
hack config get dev_host
hack config set dev_host "myapp.hack"
hack config set logs.snapshot_backend "compose"Run hack help (or hack help <command>) for full usage.
Common:
hack global install|up|downhack init|up|down|logs|open|tuihack statushack remote setuphack gateway enable
Full command table + flags: docs/cli.md.
Run hack help <command> for detailed help.
Project commands that call Docker Compose accept --profile (up/down/restart/ps/logs/run).
Use --json for machine-readable output:
hack projects --jsonhack ps --jsonhack logs --json(NDJSON stream; use--no-followfor snapshots)hack open --json(returns{ "url": "..." })
When the daemon is running, hack projects --json and hack ps --json use it for faster results.
hack logs --json emits event envelopes (start, log, end) so MCP/TUI consumers can stream safely.
hackd is a local daemon that caches Docker state for fast status/ps queries.
hack daemon start
hack daemon status
hack daemon metrics
hack daemon stop
hack daemon logsOn macOS, you can install hackd as a launchd service for automatic management:
# Install with auto-start on login
hack daemon install --run-at-load
# Install without auto-start (manual start/stop via launchd)
hack daemon install
# Uninstall the launchd service
hack daemon uninstallOptions:
--run-at-load/--no-run-at-load: Start automatically on login--gui-only/--no-gui-only: Only run in GUI sessions (default: GUI only)
The service uses label dance.hack.hackd and writes to ~/Library/LaunchAgents/.
Use it when:
- You run
hack projects --json/hack ps --jsonfrequently (scripts, agent workflows, TUI). - You want faster status snapshots without shelling out to Docker each time.
Skip it when:
- You prefer zero background processes.
- You rarely use JSON status/ps outputs.
If it is not running (or version-mismatched), the CLI falls back to direct Docker calls.
hack includes a small control-plane kernel so features like jobs, tickets, and remote access can
ship as extensions without bloating the core CLI.
- Extension commands run via
hack x <namespace> <command>. - Global control-plane config lives at
~/.hack/hack.config.json(hack config set --global ...). - Per-project overrides live in
.hack/hack.config.jsonand win over global values. controlPlane.gateway.enabledis project-scoped and implicitly enables the gateway extension.
See SPECS/control-plane/consolidated.md and docs/extensions.md for the extension SDK surface.
The gateway exposes hackd over HTTP/WS with token auth. It binds to 127.0.0.1 by default and
should be exposed via a Zero Trust/VPN or SSH tunnel when needed. hack remote setup is the
one-command flow that enables the gateway, creates a token, and can configure + start exposure
(Cloudflare/Tailscale/SSH) via prompts. It prints a QR by default (use --no-qr to skip).
hack remote setup
# or:
hack gateway setup
# or manually:
hack gateway enable
hack daemon stop && hack daemon start
hack x gateway token-createGateway tokens default to read-only. For non-GET requests, set
controlPlane.gateway.allowWrites = true globally and create a write-scoped token:
hack config set --global 'controlPlane.gateway.allowWrites' truehack x gateway token-create --scope writeCurrent gateway API (HTTP/WS):
- status/metrics/projects/ps (
/v1/*) - supervisor jobs: list/create/show/cancel + log/event stream (
/control-plane/*) - supervisor shells: create/show + PTY stream (write token + allowWrites)
- CLI shell client:
hack x supervisor shell(gateway + write token required)
Interactive shells are available over the gateway WebSocket; the CLI can attach with
hack x supervisor shell (write token + allowWrites required). Use SSH/Zero Trust for a full
terminal UI, or run commands via supervisor jobs.
See docs/gateway-api.md for full API usage, structured workflow patterns, and a runnable demo.
Remote access options (recommended order):
- SSH tunnel to the gateway port for quick, ad-hoc access.
- Zero Trust/VPN (Tailscale, Cloudflare, etc.) for persistent access.
- Optional Caddy route (
https://gateway.hack) for local convenience.
Note: Cloudflare Tunnel is ideal for the gateway HTTP/WS surface. It is not a direct SSH replacement on iOS; use Tailscale/VPN for SSH access from mobile clients. If you already use Cloudflare WARP, configure a private network route to your laptop and use that IP/hostname for SSH in your mobile client.
Remote helper:
hack remoteshows status and offers to run setup when needed.hack remote statusprints gateway + exposure status.hack remote qrprints a QR payload for SSH or gateway usage (confirm before sharing).hack remote monitoropens a mini TUI (status + gateway audit log tail).
Cloudflare tunnel helper:
hack x cloudflare tunnel-setup --hostname gateway.example.com
hack x cloudflare tunnel-startTailscale helper:
hack x tailscale setup
hack x tailscale statusDNS note: cloudflared tunnel route dns <tunnel> <hostname> creates the required CNAME to
<tunnel-id>.cfargotunnel.com in your Cloudflare zone (proxied).
The supervisor is the job/shell runner that powers agent workflows and remote execution. It can run
commands, stream logs, and host PTY-backed shells. Use it locally with hack x supervisor or
remotely over the gateway.
Docs: docs/supervisor.md.
Start here:
docs/README.md(index)docs/architecture.mddocs/gateway.mddocs/extensions.md
Use hack setup to install local integrations. Default is project scope; add --global for user scope.
hack setup cursor
hack setup claude
hack setup codex
hack setup agentsWhat each setup command does:
hack setup cursor: installs Cursor rules in.cursor/rules/hack.mdchack setup claude: installs Claude Code hooks in.claude/settings.local.json(or user scope)hack setup codex: installs the Codex skill in.codex/skills/hack-cli/SKILL.mdhack setup agents: adds/updates hack usage snippets inAGENTS.mdandCLAUDE.mdhack setup mcp: writes MCP configs for no-shell clients
Primer helpers:
hack agent prime: short CLI-first primer used by Claude Code hookshack agent init: repo-specific setup prompt agents can follow to scaffold/verify hack confighack agent init --client cursor|claude|codex: open the prompt directly in an agent clienthack agent patterns: dependency/ops checklist for agents
Recommended flow:
- Use
hack setup cursor|claude|codexfor your agent client - Use
hack setup agentsto document hack usage inside the repo - Use
hack setup mcponly when the agent has no shell access
Use MCP only when the CLI is not available (e.g. no shell access).
Run the MCP server locally over stdio:
hack mcp serveMost MCP clients spawn this on demand. Running it directly will wait for a client connection.
Install MCP configs (Cursor, Claude CLI, Codex):
# Project-scoped (default via setup)
hack setup mcp
# User-scoped (default; writes to your home config directories)
hack mcp install --all
# Project-scoped (writes .cursor/.claude/.codex in the repo)
hack mcp install --all --scope projectProject scope writes .cursor/mcp.json, .claude/settings.json, and .codex/config.toml.
Print config snippets without writing:
hack mcp print --codexhack init can prompt to install local agent integrations after scaffolding a repo.
For agent-driven scaffolding without prompts, use hack init --auto and run hack setup manually.
Use --branch <name> on project commands to run isolated instances with unique hostnames and compose
project names:
hack up --branch feature-x --detach
hack logs --branch feature-x
hack open --branch feature-xUsing --branch will create/update .hack/hack.branches.json with a last_used_at timestamp. Branch
instances show up in hack projects --details.
Optional: track branch aliases in .hack/hack.branches.json for quick lookup:
hack branch add feature-x --note "worktree for PR 123"
hack branch list
hack branch open feature-xIf your app runs in Docker (the default in hack), don’t connect to 127.0.0.1 / localhost for Postgres/Redis.
Inside a container, localhost is that container, not the other compose services.
If you previously ran everything on your host and used localhost:PORT, update those references when you
move into containers:
- HTTP services: use the same
https://*.hackhostname you open from the host (whatever you configured in Caddy labels, e.g.https://api.myapp.hack). - Non-HTTP services (DB/Redis/etc.): use the Compose service hostname (e.g.
db,redis).
For HTTP services, use the same https://*.hack URLs you use on the host. hack up injects internal DNS,
TLS trust, and extra_hosts mappings so *.hack resolves reliably inside containers. If you see ENOTFOUND
inside containers, run hack restart to refresh the host mappings.
For non-HTTP services, use the Compose service hostname on the default network:
Postgres: db:5432Redis: redis:6379
Note: Caddy’s CA is mounted into containers when internal.tls: true so HTTPS calls to *.hack work for most runtimes.
If you’re using Java/Kotlin, you’ll need to import the CA into the JVM truststore manually.
Example:
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/mydb
REDIS_URL: redis://redis:6379If you need host access for debugging, prefer docker compose exec so you don’t reintroduce port conflicts:
docker compose -f .hack/docker-compose.yml exec db psql -U postgres -d mydb
docker compose -f .hack/docker-compose.yml exec redis redis-cliBy default, hack logs uses docker compose logs because it’s the lowest latency tail.
The daemon does not proxy logs yet; hack logs still talks directly to Docker Compose or Loki.
Loki is still valuable for:
- querying across time (history)
- filtering by labels (project/service/container)
- Grafana Explore / dashboards
# Tail (fast)
hack logs --pretty
# Snapshot
hack logs --no-follow --pretty
# Snapshot JSON
hack logs --no-follow --json
# Query/history (force Loki)
hack logs --loki --pretty
# Range (Loki only)
hack logs --loki --since 2h --pretty
hack logs --loki --since 4h --until 1h --pretty
# Filter Loki by service
hack logs --loki --services api,worker --pretty
# Raw LogQL
hack logs --loki --query '{project="my-project"} |= "error"' --pretty- Open:
hack open logsor visithttps://logs.hack - Explore queries:
{project="my-project"}
{project="my-project", service="api"}
Alloy labels logs with:
- project:
Docker Compose project name - service:
Docker Compose service name - container:
Docker container name
hack maintains a best-effort registry under ~/.hack/projects.json so you can target a project from anywhere:
hack projects
hack logs --project my-project --pretty
hack up --project my-projectOAuth providers (notably Google) require localhost or a host that ends with a real public suffix.
We keep .hack as the primary local dev domain, and optionally expose an alias domain for OAuth flows.
If the OAuth alias is enabled, hack global install configures *.hack.gy to resolve to the Caddy
container IP via dnsmasq + the OS resolver (bypasses port forwarding issues with Tailscale/VPNs).
If you use Next.js (or another dev server that cares about dev origins), configure its dev allowlist to include the proxy domains.
Next.js supports allowedDevOrigins (wildcards supported) in next.config.js:
module.exports = {
allowedDevOrigins: ["*.hack", "*.hack.gy"],
}Optionally you can pass in your own custom dev_host to the config.
hack uses Caddy’s internal PKI to issue certs for *.hack (and any OAuth alias host). This covers
HTTPS for services routed through Caddy, but it does not create cert/key files for services running
outside of Caddy.
- macOS: run
hack global trustto trust the Caddy Local CA in the System keychain. - Other OS: run
hack global cato export the CA cert path, then add it to your OS/browser trust store. - If you need the PEM directly:
hack global ca --print. - If you are running a local service outside of Caddy, use
hack global cert <host...>(mkcert required) to generate a cert/key under~/.hack/certsand wire it into your service. This is only needed for non-Caddy services that still want trusted TLS. - macOS:
hack global installcan optionally install mkcert (needed forhack global cert).
Install mkcert if you don't already have it (macOS example):
brew install mkcert
mkcert -installExample (non-Caddy service):
hack global cert --install api.myapp.hackUse --out <dir> if you want certs written somewhere else.
hack global install runs CoreDNS on the hack-dev network. CoreDNS answers *.hack and *.hack.* with
Caddy’s current IP so containers can use the same https://*.hack URLs as the host.
Some runtimes don’t honor custom DNS for *.hack reliably, so hack up also injects extra_hosts mappings
to the Caddy IP. If the Caddy IP changes, hack status, hack doctor, and the TUI show a warning; fix it
with hack restart to refresh the mapping.
If you also need extra_hosts for non-*.hack hostnames (common when you run host-local tunnels/proxies
and want containers to reach them by their real domain), use hack internal extra-hosts set to write a
repo-local .hack/.internal/extra-hosts.json that gets merged into the generated Compose override.
When internal.tls is enabled, hack up mounts the Caddy Local CA into each container and sets common
SSL env vars so HTTPS to *.hack is trusted inside containers.
If you update hack, rerun hack global install once to refresh the CoreDNS config.
They run containers. They don’t give you stable hostnames, HTTPS, or a way to run many isolated copies of the same stack without custom glue.
You can build that layer yourself. I did. That’s this.
Kubernetes solves cluster orchestration. This problem is local parallelism.
It adds complexity without fixing ports, routing, or developer feedback loops.
This is the default answer and it doesn’t scale.
Ports leak into config, break OAuth and cookies, and turn into debt.
Hostnames scale. Ports don’t.
There isn’t an off-the-shelf tool that gives you full local network isolation, real HTTPS, and near zero per-repo setup.
If you want that, you have to build it yourself.
hack is a thin layer on top of Docker Compose plus a tiny global proxy.
- each project/branch runs in its own Docker network
- services use their normal ports inside that network
- a shared proxy routes
https://*.hackand handles HTTPS - logs are captured centrally
Your code doesn’t change. Your mental overhead does.
hack global install provisions ~/.hack/ and starts:
- Caddy (
lucaslorentz/caddy-docker-proxy) on ports80/443- watches Docker labels and auto-routes
https://*.hack
- watches Docker labels and auto-routes
- Logging stack: Grafana + Loki + Alloy
- reachable via
https://logs.hack
- reachable via
hack init creates .hack/ in the repo root:
.hack/docker-compose.yml: your project services.hack/hack.config.json: project config (name, dev host, log preferences)
Each project’s compose network stays isolated; only services you want “public” get attached to the shared ingress network so Caddy can reach them.
Agents: run hack agent patterns for a compact checklist based on this section.
If you run bun install on the host and then start Linux containers, you can hit platform
mismatches (native modules, postinstall scripts, OS-specific binaries). The clean pattern is to
install dependencies inside Docker and share them via a volume.
Add a one-shot deps service and make your app services depend on it:
deps:
image: imbios/bun-node:latest
working_dir: /app
volumes:
- ..:/app
- node_modules:/app/node_modules
command: bun install
networks:
- default
www:
image: imbios/bun-node:latest
working_dir: /app/apps/www
volumes:
- ..:/app
- node_modules:/app/node_modules
depends_on:
deps:
condition: service_completed_successfullyThis keeps dependency resolution in the same OS as your containers and avoids host/guest drift.
Because hack avoids publishing DB ports to your host, run schema/ops commands inside the
compose network.
Option A (recommended): add an ops-only service:
db-ops:
image: imbios/bun-node:latest
working_dir: /app/packages/db # where your db schema + package.json live
volumes:
- ..:/app
- node_modules:/app/node_modules
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/mydb
depends_on:
- db
- deps
networks:
- default
profiles: ["ops"]
# Examples:
# - Prisma: bunx prisma migrate deploy
# - Drizzle: bunx drizzle-kit push
command: bun run db:pushRun it on demand:
docker compose -f .hack/docker-compose.yml --profile ops run --rm db-opsOption B: run via hack run (thin wrapper over docker compose run --rm):
hack run --workdir /app/packages/db email-sync -- bunx prisma generate
hack run --workdir /app/packages/db email-sync -- bunx prisma migrate dev
hack run --workdir /app/packages/db email-sync -- bunx drizzle-kit push
hack run --workdir /app bun run turbo db:migrateIf your ops service is behind a compose profile, enable it:
hack run --profile ops --workdir /app/packages/db db-ops -- bun run db:pushSee examples:
examples/next-app/README.md
-
*.hackdoesn’t resolve: runhack doctor, thenhack global install(macOS: ensure dnsmasq is running). -
Stale global setup / CoreDNS issues: run
hack doctor --fix(refreshes network + CoreDNS + CA). -
TLS warnings: run
hack global trust(macOS). -
Logs missing in Grafana: ensure Alloy is running (
hack global status) and try{app="docker"}in Explore. -
ENOTFOUNDfor*.hackinside containers: runhack restartto refreshextra_hostsmappings (checkhack statusor the TUI for Caddy IP mismatch warnings). -
EAI_AGAINfor external domains inside containers (e.g.api.clerk.com): CoreDNS isn’t forwarding. Runhack global installand restart CoreDNS:docker compose -f ~/.hack/caddy/docker-compose.yml restart coredns. -
hack global upwarns abouthack-devnetwork labels or missing subnet: remove the network and reinstall:docker network rm hack-devthenhack global install. -
OAuth redirect errors: use the OAuth alias host (
*.hack.gy) orlocalhost(providers may reject non-public suffixes like.hack).
bun install
bun run install:dev
hack --helpThis installs a small hack shim into ~/.hack/bin/hack that runs your working tree directly (no rebuild needed).
If hack isn’t found, add this to your shell config:
export PATH="$HOME/.hack/bin:$PATH"bun install
bun run install:bin
hack --helpThis builds dist/hack via bun build --compile and installs it to ~/.hack/bin/hack.
bun run install:statusReports whether hack is a dev shim or a compiled binary (and where it points).
bun dev --helpbun testbun run commitCommitlint runs on git commit via husky, and semantic-release uses Conventional Commits to
compute versions.
bun run build
./dist/hack --helpbun run build:releaseProduces dist/release/hack-<version>-release/, a tarball, and hack-install.sh.
bun run release:prepare
git push --follow-tagsUpdates CHANGELOG.md + package.json, creates the release commit and tag, and triggers the
GitHub Release workflow on push.
See PACKAGING.md for details.
See also: