Default to using Bun instead of Node.js.
- Use
bun <file>instead ofnode <file>orts-node <file> - Use
bun testinstead ofjestorvitest - Use
bun build <file.html|file.ts|file.css>instead ofwebpackoresbuild - Use
bun installinstead ofnpm installoryarn installorpnpm install - Use
bun run <script>instead ofnpm run <script>oryarn run <script>orpnpm run <script> - Use
bunx <package> <command>instead ofnpx <package> <command> - Bun automatically loads .env, so don't use dotenv.
Bun.serve()supports WebSockets, HTTPS, and routes. Don't useexpress.bun:sqlitefor SQLite. Don't usebetter-sqlite3.Bun.redisfor Redis. Don't useioredis.Bun.sqlfor Postgres. Don't usepgorpostgres.js.WebSocketis built-in. Don't usews.- Prefer
Bun.fileovernode:fs's readFile/writeFile - Bun.$
lsinstead of execa.
Use bun test to run tests.
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});TypeScript (strict). Runtimes: Bun 1.3+, Node 23. Prettier: 2 spaces, no semicolons, double quotes, width 100. Import order: @ianvs/prettier-plugin-sort-imports + Tailwind plugin. ESLint (flat): any disallowed; unused vars warned (prefix _ to ignore). Naming: React components PascalCase; files kebab-case (e.g., user-profile.ts); packages @repo/. Comments: avoid writing inline comments everywhere, unless absolutely necessary for a todo or an important thing to take note of. Instead write tsdoc style block level comments at the method/class/function/route level. Focus on comments that provide value in regards to better type inference and clarity of usage. Using things like params/returns/etc..
Always default to useing named paramaters in functions eg myFunction({ ctx, other }) vs myFunction(ctx, other) Never use any types and always default to leveraging generics and smart types to sensure the best possible tpye inference across the project.
- ANY interface or behavior change (commands, flags, config keys, file layouts, env vars, defaults) must update the affected docs/ pages in the same patch.
- The CLI reference is generated: after changing the CLI surface, run
bun run docs:cli-referenceand commitdocs/reference/cli.md(a drift test fails otherwise). - Agent-facing behavior phrasing lives in
src/agents/instruction-source.ts; update it (not the generated surfaces) and runhack setup sync --all-scopes. - Real end-to-end coverage lives in
tests/e2e/(bun run test:e2e:local) — extend it when adding user-facing workflows.
This project uses Obsidian for project context, specs, research, and progress tracking.
Vault: ~/.vaults/core
Project folder: 30 Projects/Hack CLI/Notes/
When to write notes:
- Project analysis and architecture reviews
- Research findings (e.g., Ghostty VT, libraries, APIs)
- Specs and design decisions
- Session summaries with progress updates
- Any context that should persist across sessions
How to write notes:
- Use the
obsidianskill to interact with the vault - Name notes with date prefix:
YYYY-MM-DD — Title.md - Include tags:
#hack-cli #substrate
When ending a work session, you MUST complete ALL steps below.
MANDATORY WORKFLOW:
- File tickets for remaining work - Create tickets for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- Write session summary to Obsidian - Key decisions, progress, blockers
- Hand off - Provide context for next session
Use hack as the single interface for local-first runtime orchestration (compose, DNS/TLS, logs, env, and persistent project workspaces).
Integration freshness:
- These instructions were generated by hack CLI v3.4.1; treat cached rules from another version as potentially stale.
- At session start, audit project and global integrations with
hack setup sync --all-scopes --check. - If anything is stale, missing, or deprecated, run
hack setup sync --all-scopes, then reload the agent session so cached instructions are replaced. - Never copy or hand-edit generated Hack rules to refresh them; update the CLI and run the sync command.
- Content revision:
902b04fe1469(version alone is not a freshness guarantee).
Product boundary:
- Supported v3 surface: project init, up/down/restart, open, logs, env, host exec/shell, sessions, doctor, and daemon.
- Removed surfaces: hosted auth/account/org/team flows, web dashboard, built-in GitHub workflows, and built-in Linear sync.
- Experimental and unsupported: remote/gateway/node/dispatch commands. They are hidden from default help (list with
hack help --all) and warn on use; do not use them unless explicitly requested.
Operating rules:
- Prefer
hackover rawdocker/docker composefor project workflows. - Do not start/stop services from Docker Desktop UI for
hack-managed projects. - Treat
.hack/.internaland.hack/.branchas hack-managed artifacts; do not hand-edit generated files there. - Use
--jsonfor machine-readable output when available;hack up/down/restart/doctor --jsonemit an{ok, data | error: {code, message}}envelope with stable E_* error codes. - Scripted/agent runs: pass
--no-interactive(or setHACK_NO_INTERACTIVE=1) so commands never block on prompts — they apply documented defaults or fail fast with E_INTERACTIVE_REQUIRED. - Use MCP only when shell access is unavailable.
- If runtime state looks wrong, run
hack doctor, thenhack doctor --fixbefore manual repair.
Core objects:
- Project: a repo with
.hack/config + compose file. - Service: a compose service (e.g. api, web, worker).
- Instance: a running project; branch instances are separate copies started with
--branch.
Config + schema:
- Project config:
.hack/hack.config.json - Global config:
~/.hack/hack.config.json - Schema URL:
https://schemas.hack/hack.config.schema.json - Prefer CLI writes:
hack config get <path>,hack config set <path> <value>,hack config set --global <path> <value>
Hostname routing + Caddy labels:
- Primary host comes from
dev_host(default:<project>.hack). - Subdomain pattern is
<sub>.<dev_host>(for example:api.myapp.hack). - OAuth alias (when enabled) also routes
<dev_host>.<tld>and<sub>.<dev_host>.<tld>(default tld:gy). - Browser launches automatically prefer a routed OAuth alias when enabled; custom development hosts outside
.hackstay on the primary host. Setopen.preferor passhack open --prefer <auto|alias|dev>to override. - Not every compose service is routable: only services with Caddy labels and on
hack-devare exposed. - Required labels for HTTP services:
caddy,caddy.reverse_proxy,caddy.tls=internal. - Quick checks:
hack open,hack open <sub>,hack open --json.
TLS + valid-hostname constraints:
hackuses Caddy internal PKI for HTTPS on routed hosts; trust CA withhack global trust.- Containers get a combined public+local trust bundle (SSL_CERT_FILE etc.) once
hack global trusthas run; public TLS (package registries, external APIs) keeps working alongside*.hacktrust. - If the combined bundle is missing, only Node gets
*.hacktrust (NODE_EXTRA_CA_CERTS); OpenSSL-based tools keep public roots — runhack global trustto enable both. .hackis local-first and great for dev, but it is not a public suffix.- Use OAuth alias hosts (for example
*.hack.gy) when providers require public-suffix-style callback domains. - Alias hosts are still local-dev routes unless you add an external tunnel/remote ingress path.
Project files (managed vs generated):
- Source-of-truth files:
.hack/docker-compose.yml,.hack/hack.config.json,.hack/hack.env.default.yaml, and optional.hack/hack.env.<overlay>.yaml. - Worktree-local env override files:
.hack/hack.env.local.yamland.hack/hack.env.<overlay>.local.yaml. - Local-only files:
.hack.secret.key, optional.hack/.envcompatibility output,.hack/.env.state.json, and.hack/.internal/(runtime/local machine state; keep gitignored). - Generated (do not hand-edit):
.hack/.internal/compose.override.yml,.hack/.internal/compose.env.override.yml,.hack/.internal/compose.runtime.override.yml,.hack/.branch/compose.<branch>.override.yml,.hack/.branch/compose.<branch>.runtime.override.yml. - Managed via CLI:
.hack/.internal/extra-hosts.json(usehack internal extra-hosts ...commands). - Lifecycle runtime files:
.hack/.internal/lifecycle/state.json,.hack/.internal/lifecycle/*.log. - Ignore rules: hack owns a committed
.hack/.gitignore(self-healing on init/up) covering machine-local generated files (.internal/,.branch/,.env,.env.state.json,hack.env*.local.yaml); keep it committed, and if generated files leaked into git,hack doctor --fixuntracks them (files stay on disk).
Linked git worktrees:
- Secret key inherits from the primary checkout automatically through the shared git common dir; set
HACK_ENV_SECRET_KEYfor CI or detached environments. hack upin a linked worktree defaults to a branch instance named after the worktree's git branch; a detached linked worktree requires an explicit--branch, unless configworktree.auto_branch=falseexplicitly opts into the base instance.- Before
hack uporhack restartauto-targets a new branch instance, Hack warns when the same worktree already owns a non-terminal instance; pass--branch <name>to make the target explicit. hack doctorflags divergent secret keys and dev_host collisions across checkouts.
Advanced networking (extra_hosts + local proxies/tunnels):
- Static host mappings: set
internal.extra_hostsin.hack/hack.config.json. - Dynamic host mappings:
hack internal extra-hosts set <hostname> <target>/unset/list. - For host-local proxies/tunnels, prefer
host-gatewayas target when possible. - After mapping changes or proxy IP churn:
hack restartand thenhack doctor.
Standard workflow:
- If
.hack/is missing:hack init - Start services:
hack up --detach(orhack up -d) - Check status:
hack psorhack status - Open app URL:
hack open --json - Restart:
hack restart - Stop services:
hack down
Running things (decision guide):
- One-off command in a fresh service container (deps started as needed):
hack run <service> <cmd...>. - Command inside an already-running service container:
hack exec <service> -- <cmd...>. - Host script that needs hack-stored env:
hack host exec --env <overlay> --scope <service> -- <cmd...>— this is THE way to run repo scripts; never read .env files directly. - Interactive host shell with injected env:
hack host shell --env <overlay> --scope <service>. - Browser/host URL: use
hack open <service> --json(OAuth aliases are preferred when enabled). Container-to-container traffic should use Compose DNS.
Logs (default is compose):
- Fast tail:
hack logs --pretty - Per-service tail:
hack logs <service> - Machine snapshot:
hack logs --json --no-follow - Loki history/query:
hack logs --loki --since 2h --prettyorhack logs --loki --query '{project="<name>"}' - Force compose backend:
hack logs --compose - Global infra logs:
hack global logs caddy --no-follow --tail 200
Lifecycle + startup:
- Put host setup in
.hack/hack.config.jsonunderstartup/lifecycle(not ad-hoc terminal tabs). - Use
lifecycle.up.beforefor pre-start hooks andlifecycle.processesfor long-running host tasks. - Lifecycle hooks and processes receive the selected overlay's
globalvalues plushostoverrides; service-scoped values remain container-specific unless a host command explicitly selects that scope. - For fixed-port host helpers such as SSM tunnels or local proxies, set
singleton.portsand usuallyonConflict: "adopt"so Hack reuses a healthy existing listener instead of starting duplicate tunnel stacks. singletonis a listener guard, not process ownership transfer; adopted external processes are left running onhack down.- Inspect lifecycle status via
hack projects --detailsand stream viahack logs <service-or-process>. - Lifecycle session recovery is ownership-proven: Hack adopts healthy token-, definition-, and environment-matched sessions, replaces owned stale sessions, and refuses to kill same-name sessions without deterministic ownership proof.
hack doctor --fixreaps an orphan lifecycle session only when mux ownership is proven and its Compose instance is absent; unverified same-name sessions are never modified.- After
hack uporhack restart, running services and successful one-shot services (exitedwith code 0) count as successful. Hack recognizes dependency installers,hack.service.one-shot=true, and services referenced by Composecondition: service_completed_successfully; other states returnE_STARTUP_INCOMPLETE, andhack doctorwarns about containers stuck inCreated. - Detached startup is bounded; a hung Compose operation returns
E_STARTUP_TIMEOUT, terminates its process group, andhack doctor --fixcan start exact containers left inCreated. - Target only affected services with
hack up <service...> --detach,hack restart <service...>, orhack env apply --service <service>; scoped operations skip project lifecycle hooks and implicit dependency startup. - Use
hack env explain <KEY> --env <overlay> --service <service> --target <host|compose>for redacted source, precedence, availability, and delivery diagnostics. - Dependency installer services are detected generically by command or
hack.dependencies.bootstrap=true; registry env references are preflighted before container mutation. Optionalhack.dependencies.cache-volume,hack.dependencies.lockfiles, andhack.dependencies.runtime-fileslabels enable lockfile/runtime-keyed volumes shared across compatible worktrees.
Workspaces (mux-managed, tmux-first by default):
- Picker:
hack sessionfor persistent project workspaces. - Reuse/create:
hack session start <project> - Env-scoped workspace:
hack session start <project> --env qa --service api --detach - Force isolated agent workspace:
hack session start <project> --new --name agent-1(<project>--agent-1). - Execute in workspace:
hack session exec <workspace> "<command>" - Execute in workspace with injected env:
hack session exec <workspace> --env qa --service api "bun db:migrate" - Stop workspace:
hack session stop <workspace>
Host-side env helpers:
- One-off host command with injected env:
hack host exec --env qa --scope api -- bun db:migrate - Host commands default to a host-local env view; use
--target composewhen you explicitly want container-oriented addresses. --scopeselects which env scope to inject; it does not move execution into that service container.- Interactive host shell with injected env:
hack host shell --env qa --scope api - Run inside an already-running service container:
hack exec api -- bun test
Branch instances (parallel envs):
- Use a branch instance when you need two versions running at once (PR review, experiments, migrations) or want to keep a stable environment while testing another branch.
- Target one with
--branch <name>on up/open/logs/down (for example:hack up --branch <name> --detach). - Linked worktrees pick a branch instance automatically (see Linked git worktrees).
- Containers receive
HACK_RUNTIME_METADATAplusHACK_DEV_URL,HACK_ALIAS_URL, and current-service URL fields derived from effective Caddy routes. Use Compose DNS for internal traffic and this metadata for browser-facing links, OAuth callbacks, and webhooks.
Run commands inside services:
- One-off:
hack run <service> <cmd...>(usesdocker compose run --rm) - Example:
hack run api bun test - Use
--workdir <path>to change working dir inside the container. - Use
hack ps --jsonto list services and status.
Project targeting:
- From repo root, commands use that project automatically.
- Else use
--project <name>(registry) or--path <repo-root>. - List projects:
hack projects --json
Global infra:
- Bootstrap once:
hack global install - Start/stop/status:
hack global up,hack global down,hack global status - Use
hack global upbefore Loki/Grafana queries if global logging is offline.
Daemon (optional):
- Start for faster JSON status/ps:
hack daemon start - Check status:
hack daemon status
Docker compose notes:
- Prefer
hackcommands; they include the right files/networks. - Use
docker compose -f .hack/docker-compose.yml exec <service> <cmd>only if you need exec into a running container.
Agent integration maintenance:
- Project-level hack commands auto-check integration drift and attempt auto-sync (project docs, client skills/rules, shared global skills, and MCP).
- When drift is detected, Hack reports it before repair and tells the agent to reload after repair; it never silently leaves the session using cached rules.
- Set
HACK_SETUP_SYNC_MODE=warnto only warn, orHACK_SETUP_SYNC_MODE=offto disable. - Refresh project + user integrations:
hack setup sync --all-scopes - Audit integration state only:
hack setup sync --all-scopes --check - Remove generated integration artifacts:
hack setup sync --all-scopes --remove - After upgrading CLI:
hack updatethenhack setup sync --all-scopes - When changing hack itself: interface or behavior changes must update docs/ in the same change (regenerate the CLI reference with
bun run docs:cli-reference).
Agent setup (CLI-first):
- Cursor rules:
hack setup cursor - Claude hooks:
hack setup claude - Codex skill:
hack setup codex - Refresh all local agent integrations:
hack setup sync --all-scopes - Agent-assisted onboarding:
hack init --with claude|codex|both(new repos) orhack agent onboard(existing projects) print/hand off the full setup prompt; the/hack-initskill and thehack-initMCP prompt return the same content. - Init prompt:
hack agent init(use --client cursor|claude|codex to open) - Init patterns:
hack agent patterns - MCP (no-shell only):
hack setup mcp - MCP install (explicit):
hack mcp install --all --scope project