# Core Docs Core docs are the default path through `hack`. This section stays focused on local project orchestration: initialize a repo, run it on an isolated network, reach it on stable local hostnames, manage env and lifecycle, and keep working sessions alive. You do not need gateway, remote nodes, or extension setup to use this path. ## Start here - [Initialize a project](/docs/v4/guides/init-project) — or let an agent drive it with `hack init --with claude|codex|both` (existing projects: `hack agent onboard`) - [Architecture](/docs/v4/architecture) - [Env & secrets](/docs/v4/env) - [Lifecycle](/docs/v4/lifecycle) - [Sessions](/docs/v4/sessions) - [CLI overview](/docs/v4/cli) — supported command surface and agent/scripted ergonomics ## Day-to-day core workflows - Install + quick start: [root README](https://github.com/hack-dance/hack/blob/91b14bf0abb17602142425de0d02628a3e8a2615/README.md) - Local runtime checks: - `hack up --detach` - `hack open` - `hack logs --pretty` - `hack status` / `hack ps` - `hack down` / `hack restart` - If runtime state looks wrong, run `hack doctor`, then `hack doctor --fix` before manual repair. - `hack up`/`down`/`restart`/`doctor` accept `--json` for a stable `{ok, data | error: {code, message}}` envelope with `E_*` error codes, and a global `--no-interactive` (or `HACK_NO_INTERACTIVE=1`) so scripted/agent runs never block on prompts. - In a linked git worktree, `hack up` defaults to a branch instance named after the worktree's git branch (`worktree.auto_branch`), and the secret key is inherited automatically from the primary checkout via the shared git common dir. A detached linked worktree has no branch name to derive; pass `--branch `, or set `worktree.auto_branch=false` when intentionally targeting the base instance. ## When to leave core Move to [Beta workflows](https://github.com/hack-dance/hack/blob/91b14bf0abb17602142425de0d02628a3e8a2615/docs/beta.md) only when you intentionally want unsupported experimental: - gateway exposure - remote nodes - remote execution - remote supervisor jobs Move to [Extensions & reference](/docs/v4/reference) when you need the full command table, extension authoring details, or API-level reference material. --- # Initialize a project This sets up a repo so it can run under hack. ```bash cd /path/to/repo hack init hack up --detach hack open ``` Notes: - `hack init` writes `.hack/` files (Compose + config). - `hack init` also scaffolds `.hack/hack.env.default.yaml`. See `docs/env.md`. - `hack up` starts the stack on an isolated network. - `hack open` resolves the routed URL via the global proxy and prefers the OAuth alias when `oauth.enabled` is true. Set `open.prefer` or pass `--prefer auto|alias|dev` to override. Optional: - `hack logs --pretty` for log tailing. - `hack tui` for the interactive dashboard. - Configure log retention in `hack.config.json` via `logs.retention_period` (e.g. `7d`) and `logs.clear_on_down`. - Add startup hooks/host processes in `.hack/hack.config.json` under `lifecycle` (or `startup` shorthand). See `docs/lifecycle.md`. - For fixed-port host helpers such as SSM tunnels, add `singleton.ports` and usually `onConflict: "adopt"` so `hack up` reuses an already-running equivalent helper instead of starting duplicate tunnel stacks. Note: - Inside containers, `localhost` points at the container itself. Update any `localhost:PORT` references to: - HTTP services via `https://*.hack` hostnames (matching your Caddy labels) - non-HTTP services via Compose service hostnames (e.g. `db`, `redis`) ## What discovery checks (and what it can't) `hack init` (interactive and `--auto`) discovers dev scripts across a repo/monorepo and runs a validation pass over the results before writing `.hack/docker-compose.yml`. It catches common scaffolding mistakes automatically, but it is not a substitute for reviewing the generated compose file: - **Duplicate/aggregator script dedupe**: when a package defines both `dev` and `start` (or similar), only the best-scoring script becomes a service — no more `web` + `web-2`. Root-level "aggregator" scripts that just delegate to a workspace package's own dev script (e.g. `turbo run dev --filter=web`, or `dotnet run --project apps/backend`) are also dropped in favor of the package-local script. In the interactive wizard, the deduped set is pre-selected by default — you can still add a dropped script back manually. - **Port reassignment**: HTTP services that would collide on the same internal port are deterministically reassigned to the next free port (ascending from the collision), and the container command is rewritten to match. Each reassignment is logged as a warning. - **Runtime TODOs**: services whose dev script looks non-JS (`.csproj`/`.fsproj`, `go.mod`, `Cargo.toml`, `pyproject.toml`/`requirements.txt`, `mix.exs`, `Gemfile`, or a command like `dotnet run`/`go run`/`cargo`/`python`/`mix`) get an obviously-wrong placeholder image (`alpine:3`) plus a `TODO(hack-init): ...` comment above the service in the compose file — instead of silently getting the default Bun/Node image. You must replace the image and command by hand. - **Native file watching**: generated services use the container runtime's native file notifications. Hack does not force Chokidar or Watchpack polling because polling a bind-mounted monorepo can consume substantial CPU and disk I/O. Add `CHOKIDAR_USEPOLLING` or `WATCHPACK_POLLING` to an individual service only when its runtime has a demonstrated file-notification problem. - **Backing-service warnings**: dependencies (`pg`, `ioredis`, `@temporalio/*`, `kafkajs`, `amqplib`, `mongodb`/`mongoose`, `prisma`, ...) and `.env`/`.env.example` key names (never values) are scanned for signals of postgres, mysql, redis, temporal, kafka, rabbitmq, or mongodb. These are **not** auto-scaffolded — they show up as warnings and as a comment block at the top of the generated compose file with a one-line suggestion each. If a `docker-compose*.yml`/`compose*.yml` already exists at the repo root, it's flagged too ("treat it as ground truth for backing services and images") instead of being re-derived from scratch. None of this replaces reviewing the scaffold. Discovery is a best-effort heuristic pass, not ground truth — always inventory the generated `.hack/docker-compose.yml` against the real repo before running `hack up` (agents included; see the onboarding prompt for the inventory-first review step). When the local path is working and you intentionally want remote execution or gateway exposure, move to [Beta workflows](https://github.com/hack-dance/hack/blob/91b14bf0abb17602142425de0d02628a3e8a2615/docs/beta.md). For full command lookup and extension docs, use [Extensions & reference](/docs/v4/reference). --- # Env & secrets Hack now uses repo-level env overlay files as the canonical project env model. The short version: - commit `.hack/hack.env.default.yaml` - optionally commit `.hack/hack.env..yaml` - use `.hack/hack.env.local.yaml` and `.hack/hack.env..local.yaml` for worktree-local overrides - keep `.hack.secret.key` out of git, or provide `HACK_ENV_SECRET_KEY` - let `hack up`, `hack run`, `hack restart`, `hack host exec`, `hack host shell`, and env-aware session flows inject values directly at runtime - use `hack env materialize` only when you explicitly need a compatibility `.hack/.env` ## Current model Canonical env state lives at the repo root: - `.hack/hack.env.default.yaml` - `.hack/hack.env..yaml` - `.hack/hack.env.local.yaml` - `.hack/hack.env..local.yaml` Legacy compatibility note: - if an older repo already tracks `.hack/hack.env.local.yaml` as the shared `--env local` overlay, Hack keeps reading that file as the shared overlay - in that compatibility case, worktree-local default overrides are written to `.hack/hack.env.default.local.yaml` so `--local` mutations do not overwrite tracked shared config Compatibility and local-only state still lives under `.hack/`: - `.hack/.env` - `.hack/.env.state.json` - `.hack/.internal/...` Encryption key material is local-only by default: - `.hack.secret.key` - or `HACK_ENV_SECRET_KEY` `hack init` now scaffolds `.hack/hack.env.default.yaml` by default. ## Ignore rules (committed `.hack/.gitignore`) Hack owns a committed `.hack/.gitignore` that keeps machine-local generated files out of git. Because the file is committed, fresh clones and linked git worktrees inherit the rules with zero setup. It covers (patterns relative to `.hack/`): - `.internal/` - `.branch/` - `.env` - `.env.state.json` - `hack.env.local.yaml` - `hack.env.*.local.yaml` - `tickets/` (retained only to contain retired machine-local ticket caches during upgrades) How it is maintained: - written by `hack init`, and self-healed by `hack up` and whenever hack writes `.hack/.internal/` overrides or `--local` env overrides - the entries live inside a marked, hack-managed block; anything you add outside the markers is preserved on every rewrite - the root `.gitignore` still gets a `.hack.secret.key` entry when the secret key is first generated (the key lives at the repo root, not under `.hack/`) - older repos that added `.hack/.internal/` to the root `.gitignore` keep working; the entries are equivalent and nothing is removed Leak detection and repair: - `hack doctor` runs a "generated files" check: it lists any of the paths above (including retired `.hack/tickets/` caches, plus `.hack.secret.key`) that are tracked in git, except a tracked `.hack/hack.env.local.yaml`, which is never flagged because older repos may intentionally track it as the shared `--env local` overlay (see the legacy compatibility note above) - a tracked `.hack.secret.key` is reported as an error — it is a committed secret; rotate it after untracking - `hack doctor --fix` untracks the offenders with `git rm --cached` (files stay on disk) and re-ensures `.hack/.gitignore`; commit the removal - `--fix` and `--migrate-env-config` cannot be combined with `--json`; run the repair and JSON audit separately so a mutating request is never silently ignored. run them separately from a JSON-mode invocation ## File format Each env file is YAML with: - `version: 1` - `environment` - `secretsprovider: project_key` - `values` `values` is split by scope: - `global`: values applied everywhere - ``: values applied only for that compose service - `host`: optional overrides applied to host execution (`hack host exec`, `hack host shell`, and lifecycle hooks/processes) Example: ```yaml version: 1 environment: default secretsprovider: project_key values: global: GLOBAL_FLAG: "true" API_BASE_URL: "https://api.example.com" api: PORT: "4000" SERVICE_TOKEN: secure: v1:... ``` Plaintext values are written as scalars. Secret values are written as `{ secure: ... }`. ## Merge rules Hack resolves env in this order: 1. load `.hack/hack.env.default.yaml` 2. if `--env=` is selected, load `.hack/hack.env..yaml` 3. load `.hack/hack.env.local.yaml` 4. if `--env=` is selected, load `.hack/hack.env..local.yaml` 5. merge `values.global` 6. if a service scope is requested, merge `values.` on top Worktree-local overrides win over shared repo overlays. Service values override global values. Host execution applies `host` values last. This includes `hack host exec`, `hack host shell`, and lifecycle hooks/processes, which all run on the host rather than in Compose. Projects can set a default overlay in `.hack/hack.config.json`: ```json { "env": { "defaultOverlay": "qa" } } ``` Use `--env=base` to bypass that default and read only `.hack/hack.env.default.yaml`. ## Runtime behavior Direct runtime injection is the default path. Hack reads the canonical YAML files and injects the resolved env directly into: - `hack up` - `hack run` - `hack restart` - lifecycle host processes - `hack host exec` - `hack host shell` - `hack session start --env ... --service ...` - `hack session exec --env ... --service ...` That means `.hack/.env` is no longer the primary runtime source of truth. `hack env materialize` is manual by design. Use it only when you need a compatibility file for an external tool that expects `.env` on disk. For host execution, `hack host exec`, `hack host shell`, and lifecycle hooks/processes use a host-local view. That means Hack prefers host-usable values when it can: - explicit `host` scope values override the normal `global` + `` merge - common container-only hostnames such as `host.docker.internal` and compose service hosts are rewritten to `127.0.0.1` Use `--target compose` if you explicitly want the container-oriented compose view instead. ## Choosing the right execution surface - `hack host exec` / `hack host shell`: run on your host machine with Hack-resolved env injected. - `hack run ...`: run a one-off command in the compose network with an ephemeral service container. - `hack exec ...`: run inside an already-running container when you specifically need that container's live filesystem or process context. For host execution, `--scope` means "which env scope should Hack inject", not "where should this run." For example, `hack host exec --scope api -- bun db:migrate` still runs on the host. ## Common commands Inspect resolved env: ```bash hack env list hack env list --env qa hack env list --env qa --service api hack env list --json hack env list --show-secrets hack env explain GITHUB_TOKEN --env runner --service installer --target compose ``` `hack env explain` never prints the value. It reports availability, secret classification, selected/effective scope, source file and precedence, and whether the value is delivered to host lifecycle code or Compose. Recreate just one service after an env change with: ```bash hack env apply --service api --env qa ``` `--show-secrets` prints secret values in plaintext instead of the masked default; use it deliberately. Add or update values: ```bash hack env add API_BASE_URL https://api.example.com hack env add SERVICE_TOKEN abc123 --service api --secret hack env add --env qa API_BASE_URL https://qa.example.com hack env add API_BASE_URL https://api.example.com --local ``` `hack env set` still works as a compatibility alias for `hack env add`, but `add` is the primary UX now. `--local` writes to the worktree-local override file (`.hack/hack.env.local.yaml` or `.hack/hack.env..local.yaml`) instead of the shared repo env file — use it for per-checkout values you don't want to commit. If you omit `key`, `value`, or `--service`, `hack env add` prompts for them interactively. Pass `--no-interactive` (or set `HACK_NO_INTERACTIVE=1`) for scripted/agent usage: the command applies documented defaults or fails fast with `E_INTERACTIVE_REQUIRED` instead of blocking on a prompt. `hack env unset` and `hack env set` accept `--local` and `--no-interactive` too. Remove a value: ```bash hack env unset API_BASE_URL hack env unset SERVICE_TOKEN --service api --env qa hack env unset API_BASE_URL --local ``` Materialize a compatibility `.hack/.env`: ```bash hack env materialize hack env materialize --env qa hack env materialize --env qa --service api ``` Run a host command with injected env: ```bash hack host exec -- bun db:migrate hack host exec --env qa --scope api -- bun db:migrate hack host exec --env qa --scope api --target compose -- bun test ``` Cancelling `hack host exec` or `hack env exec` with SIGINT or SIGTERM forwards the signal to the command. Commands run in an owned process group, so cancellation also stops their descendants, escalating to SIGKILL after two seconds if necessary. Hack returns 130 for SIGINT and 143 for SIGTERM. Interactive commands use a supervisor in their own foreground group on the same terminal, preserving stdin, separate output streams, and Ctrl-Z/foreground resume. Piped stdin also retains the controlling terminal, so commands can open `/dev/tty` for native authentication prompts. The supervisor holds group ownership until cancellation cleanup finishes. SIGKILL cannot be forwarded; supervisors must terminate the whole owned process tree when force-killing a wrapper. Commands have no implicit time limit, and normal completion preserves the command's exit status. When you want to inspect an injected value, avoid `hack host exec -- echo $VAR`. Your current shell expands `$VAR` before Hack starts the child process, so the command often sees an empty string. Use one of these instead: ```bash hack host exec -- printenv APPLE_TEAM_ID hack host exec -- sh -lc 'printf "%s\n" "$APPLE_TEAM_ID"' hack host exec --shell 'echo $APPLE_TEAM_ID' ``` Open a host shell with injected env: ```bash hack host shell hack host shell --env qa --scope api hack host shell --env qa --scope api --target compose ``` Example with an explicit host override: ```yaml version: 1 environment: default secretsprovider: project_key values: global: DATABASE_URL: secure: v1:... REDISHOST: redis host: REDISHOST: "127.0.0.1" ``` ## Sessions and env Session flows can now carry env selection too. Examples: ```bash hack session start my-project --env qa --service api --detach hack session exec my-project.env-qa.svc-api --env qa --service api "bun db:migrate" ``` When you pass `--env` or `--service`, Hack creates a stable scoped workspace name instead of reusing the plain default workspace. That prevents one shell from silently inheriting the wrong env selection. ## Secret key handling Secret values in the YAML files are encrypted with the project key. Default behavior: - generate `.hack.secret.key` the first time you add a secret - add `.hack.secret.key` to `.gitignore` - decrypt secrets from `.hack.secret.key` on the local machine - linked git worktrees can reuse the primary checkout's `.hack.secret.key` when the worktree copy is missing Linked worktree behavior: - when a repo uses linked git worktrees, Hack prefers a shared key under the git common dir - sibling worktrees can decrypt the same committed secrets without manually copying gitignored files - if a checkout-local `.hack.secret.key` exists, it still wins for that checkout - `hack up` in a linked worktree auto-selects a branch instance (`worktree.auto_branch`); that selection determines which instance's resolved env is the one actually injected, so keep the key readable from every checkout that runs `hack up` Full read order for decrypting secrets (first match wins): 1. checkout-local `.hack.secret.key` 2. shared key under the git common dir 3. the primary checkout's key, inherited automatically for a linked worktree that has neither of the above 4. `HACK_ENV_SECRET_KEY` Write-path guarantees in a linked worktree (first secret added from any checkout): - an existing checkout-local key is reused as-is - otherwise an existing shared key (git common dir) is reused - otherwise the primary checkout's key is adopted by copying it to the shared location, so sibling checkouts converge on one key - only then is a new key generated, and it is written to the shared location — never silently to the checkout - if the shared location cannot be resolved (git unavailable) or written, Hack falls back to a checkout-local key and prints a loud divergence warning - `hack doctor` flags divergent `.hack.secret.key` contents across checkouts with the exact paths CI and managed container fallback: - if `.hack.secret.key` is missing, Hack falls back to `HACK_ENV_SECRET_KEY` That makes this model usable in CI and portable container images without committing the key file. Published container paths: - slim managed-container base: `hackdance/hack:slim` — the supported CI / managed-container story - full remote runtime: `hackdance/hack:latest` — serves the remote/node surface, which is experimental and unsupported (hidden behind `hack help --all`); only relevant if you have explicitly opted into that surface In both cases, inject `HACK_ENV_SECRET_KEY` from the runtime or secret manager instead of copying `.hack.secret.key` into the image. ## Legacy repos and migration Older repos may still have: - `.hack/hack.env.json` - `.hack/.env` - `.hack/.env.` - configured secret backends via `hack env backend` Hack still detects and reads that legacy format, but the preferred migration path is: ```bash hack doctor --migrate-env-config ``` That migrates the repo into: - `.hack/hack.env.default.yaml` - `.hack/hack.env..yaml` - `.hack.secret.key` when needed `hack doctor` also warns when a repo still depends on the old format. For modern env repos, `hack doctor` also warns when materialized compatibility output in `.hack/.env` or `.hack/.env.state.json` is stale and points to `hack env materialize`. ## Compatibility notes The modern YAML env model is the canonical repo format today. Some older surfaces are still compatibility-oriented: - `hack env backend ...` remains relevant for legacy repos and older secret-store configurations - the daemon's env HTTP route (used by the macOS app) reads and writes the legacy dotenv-shaped contract (`source: 'plain_env'|'keychain'`, `resolvedFrom: 'dotenv'/'process'/'keychain'/'portable_backend'`), not the modern YAML `hack env list --json` shape; there is no web dashboard surface anymore If you are writing new docs or new project setup flows, document the YAML overlay model first and treat `.hack/hack.env.json` as legacy migration context. ## Recommended project policy - Commit `.hack/hack.env.default.yaml` - Commit `.hack/hack.env..yaml` when the overlay is team-shared - Never commit `.hack.secret.key` - Prefer `hack env add` over hand-editing encrypted values - Prefer direct runtime injection over materializing `.hack/.env` - Use `hack host exec` or env-aware sessions for host-side scripts like migrations, generators, and admin tasks ## Related docs - [Initialize a project](/docs/v4/guides/init-project) - [Sessions](/docs/v4/sessions) - [CLI reference](/docs/v4/cli) - [Pulumi-style env config design](https://github.com/hack-dance/hack/blob/91b14bf0abb17602142425de0d02628a3e8a2615/docs/plans/2026-03-27-pulumi-style-env-config-design.md) ### Host command lifetime and diagnostics `hack host exec` and `hack env exec` record payload-free execution metadata under `$HACK_HOME/host-commands` (normally `~/.hack/host-commands`). Records contain the executable basename, project, process identities, owned process group, intended lifetime, elapsed time, exit/cancellation/timeout outcome, and final child CPU/RSS accounting. Arguments, shell strings, environment values and command output are not recorded. Completed records expire after seven days during later executions; interrupted records remain available for review. A diagnostic storage failure warns on stderr and preserves command execution and exit status. ```sh hack host exec --timeout 60 -- bun scripts/check.ts # Lifecycle (startup hooks + host processes) Many projects need host-side setup around `hack up` (auth, local proxies, tunnels). Lifecycle config lets you run short-lived commands and manage long-running host processes alongside the project runtime. Lifecycle is configured in `.hack/hack.config.json` under `lifecycle`. You can also use a shorthand `startup` array for common `hack up` startup flows. ## Why `singleton` exists Some lifecycle-managed helpers bind fixed local ports and are easy to start outside Hack: - AWS SSM port-forwarding tunnels - local API or database proxies - search/dashboard tunnels Without an ownership hint, `hack up` can only assume it should launch the configured command again. If a matching helper is already running manually, or a previous reconnect loop already restored it, that can create duplicate supervisors, partial port conflicts, or aggressive cleanup/restart churn. `singleton` exists to make those fixed-port helpers explicit. It gives Hack a conservative rule: - start the helper when none of its expected listeners exist - adopt the existing helper when the full listener set is already healthy and the config opts into reuse - fail fast on partial occupancy instead of trying to replace part of a live tunnel stack That keeps Hack local-first and predictable. It avoids turning "something equivalent is already running" into "kill and recreate it", which is especially important for brittle port-forwarding workflows. ## Config ```json { "$schema": "https://schemas.hack/hack.config.schema.json", "lifecycle": { "up": { "before": [ { "name": "aws sso login", "command": "aws sso login", "cwd": "." }, { "name": "aws-ssm-proxy", "command": "bun run proxy", "cwd": "packages/infra", "persistent": true, "singleton": { "ports": [3306, 9200, 9201, 8443, 8444, 8445], "onConflict": "adopt" } } ], "after": [] }, "down": { "before": [], "after": [] }, "processes": [ { "name": "aws-proxy", "command": "bun run dev:aws-proxy", "cwd": "." } ] } } ``` ## Startup shorthand `startup` is a concise alias for startup hooks/processes: - `persistent: false` (default) maps to `lifecycle.up.before` - `persistent: true` maps to `lifecycle.processes` ```json { "$schema": "https://schemas.hack/hack.config.schema.json", "startup": [ { "name": "aws sso", "run": "aws sso login", "persistent": false }, { "name": "aws-ssm-proxy", "run": "cd packages/infra && bun run proxy", "persistent": true, "singleton": { "ports": [3306, 9200, 9201, 8443, 8444, 8445], "onConflict": "adopt" } } ] } ``` Each startup item can be: - a string command (one-shot startup hook), or - an object with: - `run` (or `command`) required - `name` optional - `cwd` optional - `persistent` optional boolean (default `false`) - `singleton` optional object with `ports` and optional `onConflict` `cwd` is always resolved from the repo root (not from `.hack/`). A `persistent: true` startup entry with no `name` gets a generated name (`startup-1`, `startup-2`, ... in config order). That generated name is what shows up as the window name and what you pass to `hack logs `, so naming persistent entries explicitly makes logs easier to find. ### Hooks Hook lists live under: - `lifecycle.up.before` - `lifecycle.up.after` - `lifecycle.down.before` - `lifecycle.down.after` Each entry can be either: - a string (shell command), or - an object: - `name` (optional): label for logs - `command` (required): shell command - `cwd` (optional): working directory; relative paths are resolved from repo root - `persistent` (optional): `true` means "start this as a managed lifecycle process" - `singleton` (optional): local listener policy with: - `ports` required array of local TCP listener ports - `onConflict` optional `"adopt"` or `"fail"` (default `"fail"`) Hooks run on the host as `sh -c `. Failures stop the operation. Commands inherit the CLI process environment (including PATH), plus resolved project env vars. `persistent` behavior: - In `lifecycle.up.before`, `persistent: true` starts the command in the lifecycle mux session and immediately continues to the next hook. - Ordering is preserved: each persistent hook is started in sequence before moving to later hooks. - In other hook lists (`up.after`, `down.before`, `down.after`), `persistent` is ignored and the hook runs as a normal blocking command. `singleton` behavior: - Hack checks the configured local TCP listener ports before starting the lifecycle process. - If none of the configured ports are in use, Hack starts the process normally. - If all configured ports already have listeners and `onConflict` is `"adopt"`, Hack skips startup, records an adoption note, and leaves the external process alone on `hack down`. - If only some configured ports are already occupied, Hack fails fast instead of launching a competing partial replacement. - If all configured ports already have listeners and `onConflict` is omitted or `"fail"`, Hack stops with an explicit error so the operator can decide whether to stop or reuse the existing process. Important boundaries: - `singleton` is a listener-level guard, not a deep process identity check. - Adoption means "Hack observed the full expected local listener set and will not start another copy". It does not mean Hack now owns or can safely tear down that external process. - `hack down` only stops processes Hack actually launched inside the lifecycle session. Adopted external listeners are intentionally left alone. ### Processes Long-running processes live under `lifecycle.processes` and are objects with: - `name` (required): stable identifier (used for window naming) - `command` (required): shell command (run in a mux session shell) - `cwd` (optional): working directory (defaults to repo root) - `singleton` (optional): listener-ownership policy with `ports` and `onConflict` Processes receive the resolved env contract (see `env.md`) as their environment. ## Visibility Lifecycle output is now surfaced across CLI/runtime views: - `hack logs` includes lifecycle hook/process output alongside compose logs. - `hack logs ` supports lifecycle service names, including persistent process names. - `hack projects --details` includes a `Startup & lifecycle` section with hooks + persistent processes. - Runtime inventories include persistent lifecycle processes as synthetic services. - The macOS project detail view includes a `Startup & Lifecycle` section in the main project overview. ## Runtime behavior ### `hack up` 1. Resolve env contract (and optionally prompt for missing required env in interactive shells). 2. Run `lifecycle.up.before` hooks in order. 3. For `up.before` hooks with `persistent: true`, start each as a managed lifecycle process and continue immediately. 4. Start lifecycle processes (if any) inside the same dedicated lifecycle session. 5. Run `docker compose up` (or `up -d` when `--detach`). 6. Run `lifecycle.up.after` hooks. Before starting persistent work, Hack reconciles the expected lifecycle session with `.hack/.internal/lifecycle/state.json`: - A session whose mux ownership token, saved definition hash, and live process windows all match is adopted without starting duplicate processes. - A token-owned stale session is replaced. Pre-ownership tmux sessions are replaced only when saved state, checkout path, creation time, and window count provide deterministic legacy ownership proof. - A same-name session without matching ownership proof is left untouched and `hack up` fails with an actionable collision error. If the operation later fails in Compose or `up.after`, Hack removes only the lifecycle session created by that operation. The same exact-token cleanup runs for supported `SIGINT` and `SIGTERM` paths, including signals received while the mux session is still being initialized. ### `hack down` 1. Run `lifecycle.down.before` hooks. 2. Run `docker compose down`. 3. Stop lifecycle processes by killing the lifecycle session. 4. Run `lifecycle.down.after` hooks. If `lifecycle.down.before` fails, shutdown is aborted before Compose or lifecycle processes are stopped. `hack restart` preserves the same guard semantics during its down phase. ### `hack restart` `hack restart` runs the down lifecycle hooks and stops owned host processes, but preserves the current Compose runtime until preflight succeeds. It then force-recreates services and attempts a repair start if recreation fails. This avoids destroying a healthy stack before env and registry checks have passed. `hack restart ` is service-scoped: it skips project-wide lifecycle hooks, initializes declared shared dependency caches before replacing their selected consumers, uses `--no-deps`, and verifies only the selected services. Failed cache initialization leaves the existing consumers running. From the primary checkout, it targets only the base Compose/lifecycle instance. A linked worktree uses its isolated derived branch instance, and `--branch ` targets only that explicit branch. ### `--json` `hack up`, `hack down`, and `hack restart` accept `--json`. A lifecycle hook failure surfaces as `{ok: false, error: {code: 'E_LIFECYCLE_FAILED', message}}`; a `docker compose` failure surfaces as `E_COMPOSE_FAILED`. If Compose exits successfully but one or more containers remain `created`, exit non-zero, or enter another failed runtime state, Hack returns `E_STARTUP_INCOMPLETE`. Successful one-shot services (`exited` with code 0) are reported under `services.completed` and do not fail startup. Hack recognizes dependency installers, services labeled `hack.service.one-shot=true`, and services referenced by another service with Compose `condition: service_completed_successfully`. An unrelated service that merely exits zero is still treated as incomplete startup. Lifecycle failures are one of the primary consumers of the `--json` error envelope, since hook and process startup are common failure points around `hack up`. ## Sessions backend Lifecycle processes run inside the configured mux backend (tmux or zellij), using the same selection rules as `hack session`. Config: - Project: `.hack/hack.config.json` → `sessions.mux = auto|tmux|zellij|none` - Env override: `HACK_SESSIONS_MUX=auto|tmux|zellij|none` Lifecycle session name: - No branch: `--lifecycle` - With `--branch `: `--lifecycle-` Lifecycle hooks and processes run on the host. They receive the selected overlay's `global` values plus `host` overrides, with `host` taking precedence. Service-scoped values remain container-specific unless a host command explicitly selects that scope with `hack host exec --scope `. Notes: - If no mux backend is available, lifecycle process startup fails with an actionable error. - Teardown is implemented by killing the lifecycle session; anything running inside that session will be stopped. - Current lifecycle sessions carry the same random ownership token in mux metadata and persisted state. Cleanup requires an exact token match; deterministic names alone never authorize session teardown. - Healthy-session adoption also matches a stable SHA-256 fingerprint of the effective overlay and sorted lifecycle environment. Environment names, keys, and values are never persisted; changing an overlay or value replaces the owned session so host helpers cannot retain stale configuration. - For tmux-backed lifecycle sessions, Hack also persists the pane PID and the wrapped command's actual process-group metadata to `.hack/.internal/lifecycle/state.json`, with per-hook/process output logged to `.hack/.internal/lifecycle/*.log`. If tmux pane state disappears before teardown, `hack down` still uses that persisted metadata to clean up any live lifecycle process groups instead of leaving orphaned host processes behind. - `hack doctor` reports stale lifecycle state, leaderless process groups, ownership collisions, and ownership-proven sessions with no running Compose instance. Recently updated sessions are treated as possible in-flight startups for five minutes and stay untouched. `hack doctor --fix` reaps only established orphans after rechecking runtime liveness and mux ownership; unverified same-name sessions stay untouched. - Runtime hygiene also warns when regular Compose services are stuck in `Created`, including stacks left behind by an interrupted startup. This check is diagnostic only; inspect the explicit target with `hack ps --json` before deciding whether to retry or tear it down. ## Tips - For long-running setup commands, use either `lifecycle.processes` or `lifecycle.up.before` with `persistent: true`. - For local proxy/tunnel helpers that bind fixed ports, prefer `singleton.ports` with `onConflict: "adopt"` so `hack up` can reuse an already-running external tunnel instead of racing it. - Keep non-persistent hooks short and deterministic. - Store lifecycle secrets in `hack.env.*.yaml` as encrypted `secure:` values and prefer runtime injection over relying on `.hack/.env`. - If a hook requires interactive auth (e.g. browser-based SSO), it will still work; it runs with `stdin: inherit`. - Global `--no-interactive` (or `HACK_NO_INTERACTIVE=1`) affects only the env-prompt step in `hack up` (step 1 above): with prompting disallowed, missing required env is never prompted for — the run fails fast with an actionable error instead of blocking on a prompt, following the same `--no-interactive` convention used across the CLI (documented defaults or `E_INTERACTIVE_REQUIRED`). It does not change hook `stdin` behavior — a hook that itself blocks on interactive auth still blocks under `--no-interactive`. --- # Sessions Sessions are **persistent project workspaces**. They keep a repo-root shell alive across terminal restarts, SSH reconnects, and long-running agent work so you can return to the same context instead of rebuilding it each time. The guided default is **tmux**: - `hack session` opens a tmux-first interactive picker - `hack session start ` reuses the default project workspace unless you ask for an isolated one - `hack session start --env qa --service api` creates a stable env-scoped workspace when you need a specific overlay - `hack setup tmux` installs the recommended popup binding so the picker is easy to reach from tmux Other mux backends still exist. `sessions.mux=auto` prefers tmux and falls back to zellij when tmux is unavailable, and you can explicitly set `sessions.mux=zellij` if that is your preferred backend. The interactive onboarding and pane tooling are tmux-first today. ## Quick start ```bash # Interactive picker for existing workspaces and new project workspaces hack session # Reuse the default workspace for a project, or create it if needed hack session start # Create an env-scoped workspace for one overlay/service combination hack session start --env qa --service api --detach # List active workspaces hack session list # Attach to an existing workspace by name hack session attach # Queue a command in a running workspace hack session exec "bun test" # Queue a command with injected env for that run hack session exec --env qa --service api "bun db:migrate" # Stop a workspace hack session stop # Capture recent output (defaults to active pane) hack session capture # Tail workspace output for a short window (defaults to active pane) hack session tail # List panes in a workspace hack session panes ``` ## Core flows ### Interactive picker Running `hack session` without arguments opens an interactive picker showing: - **Active workspaces**: running workspaces from the configured mux backend(s), with attached status when the backend can report it - **Available projects**: registered projects that do not currently have a default workspace When selecting an attached session, you can choose to: - **Attach**: detach other clients and take over the workspace - **Create isolated**: start a separate long-running workspace such as `project--2` This is the recommended entry point when you want to resume a project context but do not remember the exact workspace name. `hack session` with no subcommand is inherently interactive (it prompts with a picker) and is not a scripted/agent entry point. For scripted or agent-driven runs, use `hack session start ` (optionally with `--detach`) or `hack session list --json` instead of the bare picker. ### Start and create ```bash # Reuse the default project workspace (creates it if needed) hack session start my-project # Force creation of an isolated numbered workspace hack session start my-project --new # Create an isolated workspace with a custom suffix hack session start my-project --name agent-1 # Creates: my-project--agent-1 # Prepare services before creating or attaching hack session start my-project --up # Create or reuse without attaching the current terminal hack session start my-project --detach # Create a stable env-scoped workspace hack session start my-project --env qa --service api --detach ``` Default behavior is deliberate: - The first workspace for a project is just the project name, for example `my-project` - Passing `--env` or `--service` creates a stable scoped workspace name such as `my-project.env-qa.svc-api` - `--new` or `--name` creates an isolated workspace for a second agent, experiment, or review lane - `--detach` keeps the workspace alive for another tool or terminal to attach later Workspaces are created at the project's repo root, not the `.hack/` directory. ### Attach ```bash # Attach to an existing workspace hack session attach my-project # From inside tmux, this switches clients instead of nesting tmux ``` Use `attach` when you already know which workspace you want. If you are already inside tmux, hack switches clients instead of nesting tmux inside tmux. When attaching, tmux detaches other clients to avoid terminal size conflicts across multiple devices. If the workspace is running in zellij, hack attaches to that named session directly instead. ### Stop ```bash hack session stop ``` `stop` kills the underlying mux session (`backend.killSession`) outright — anything still running inside it, including a long-lived dev server or an in-progress agent task, is terminated. There is no graceful shutdown step; if you need cleanup hooks to run first, do that before stopping. ### Exec ```bash # Send a command to a running workspace hack session exec my-project "bun test" # tmux: sends the command + Enter to the active pane # zellij: opens a new pane and runs the command because there is no active-pane equivalent # Inject a selected overlay/service into the queued command hack session exec my-project.env-qa.svc-api --env qa --service api "bun db:migrate" ``` Use `exec` when you want to drive a persistent workspace from another terminal, script, or agent without attaching interactively first. If you pass `--env` or `--service`, Hack resolves the selected project env and injects it into the queued command without materializing `.hack/.env`. ### Listing panes (tmux only) Use `hack session panes` to list panes with useful metadata (target, active flag, window/pane indices, command, path). ```bash # List panes (NDJSON start/log/end) hack session panes my-project # Explicit NDJSON output (same as the default) hack session panes my-project --json # Single JSON object output hack session panes my-project --pretty ``` `--json` and `--pretty` are mutually exclusive; passing both is an error. ### Capturing output (tmux only) `hack session capture` emits NDJSON events by default for machine parsing (start/log/end). By default it targets the active pane; use `--target` to select a specific pane. Use `--pretty` for raw pane output; `--json` and `--pretty` are mutually exclusive. ```bash # Capture last 200 lines (default) as NDJSON hack session capture my-project # Capture a specific pane target and line count (tmux pane targets use `:` and `.`) hack session capture my-project --target my-project:0.1 --lines 500 # Human-friendly raw output hack session capture my-project --pretty ``` ### Tailing output (tmux only) `hack session tail` also emits NDJSON events by default and stops after `--max-ms` (default 5000), polling every `--interval-ms` (default 500). By default it tails the active pane; use `--target` to select a specific pane. `--json` and `--pretty` are mutually exclusive. ```bash # Poll capture-pane and emit only new lines for 5s (default) hack session tail my-project # Customize polling interval and max duration hack session tail my-project --interval-ms 250 --max-ms 10000 # Human-friendly raw output hack session tail my-project --pretty ``` ## Remote access with SSH Use `hack ssh` to get connection info for remote access: ```bash # Interactive: choose Tailscale or direct SSH hack ssh # Use Tailscale SSH hack ssh --tailscale # Use direct SSH with hostname hack ssh --direct --host 192.168.1.100 # Specify user and port hack ssh --host example.com --user dev --port 2222 # Connect directly to a workspace hack ssh my-session ``` ### What `hack ssh` shows 1. **SSH command**: Copy-paste command to connect 2. **QR code**: Scan with mobile SSH apps (Blink, Termius) 3. **Active workspaces**: List of active workspaces on this machine, including the active mux backend when known 4. **Action picker**: Done or connect to a workspace When you connect to an existing workspace, hack uses that workspace's backend. When you create a new one from the SSH flow, hack uses the configured default backend (`sessions.mux`). ### Tailscale setup If Tailscale is stopped or not connected, `hack ssh` will prompt to start it: ``` Tailscale is stopped ? Turn on Tailscale? › yes Starting Tailscale... Tailscale connected! ``` ### Terminal size conflicts When multiple clients attach to the same tmux workspace, terminal size can get messed up. The session commands use `tmux attach -d` to detach other clients, avoiding this issue. If you're already in tmux and want to switch workspaces without detaching: ```bash # Inside tmux: prefix + s to open session switcher # Or use the tmux command: tmux switch-client -t ``` ## Configuration Project config: `.hack/hack.config.json` ```json { "sessions": { "mux": "auto" } } ``` Values: - `auto` (default): prefer tmux, fall back to zellij - `tmux`: require tmux - `zellij`: require zellij - `none`: disable sessions Env override: `HACK_SESSIONS_MUX=auto|tmux|zellij|none` Global default: ```bash hack config set --global sessions.mux zellij ``` Recommended onboarding: ```bash # Install the tmux popup binding that launches the picker hack setup tmux # Keep tmux as the guided default, but switch globally if your team prefers zellij hack config set --global sessions.mux zellij ``` ## Daemon sessions API > **Status: unsupported experimental.** This HTTP surface is served by the gateway, which the > codebase itself labels "the unsupported experimental gateway server." It is outside the > supported v3 product contract, hidden from default `hack --help` (see `hack help --all`), and > prints a warning when the underlying `hack remote`/`hack gateway`/`hack node`/`hack dispatch` > commands are invoked. `hackd` (the daemon) itself listens on a local unix socket for the CLI's own use (fast JSON status/ps, etc.) — it does not speak HTTP. The HTTP/WS surface described below, including the `127.0.0.1:7788` examples, is served by the separate **gateway** process and requires it to be explicitly enabled; it is not part of the daemon's normal listener. The gateway exposes REST endpoints for managing **tmux** workspaces programmatically. Session handlers shell out to `tmux` directly — there is no zellij code path in the gateway/daemon session routes, even though `sessions.mux` supports zellij for local `hack session` usage. This is useful for: - Remote workspace control via the gateway - Building automation tools - Agent orchestration When accessed over HTTP, this API is served by the **gateway** and requires authentication. See `docs/gateway-api.md` for token creation and the full security model. See the [Gateway API](https://github.com/hack-dance/hack/blob/91b14bf0abb17602142425de0d02628a3e8a2615/docs/gateway-api.md) for authentication and endpoint details. ### Endpoints | Method | Path | Description | | --- | --- | --- | | GET | `/v1/sessions` | List all workspaces | | POST | `/v1/sessions` | Create a new workspace | | GET | `/v1/sessions/:id` | Get workspace details | | POST | `/v1/sessions/:id/stop` | Stop (kill) a workspace | | POST | `/v1/sessions/:id/exec` | Execute command in a workspace | | POST | `/v1/sessions/:id/input` | Send raw keystrokes | ### List workspaces ```bash curl -H "Authorization: Bearer $HACK_GATEWAY_TOKEN" \ http://127.0.0.1:7788/v1/sessions ``` Response: ```json { "sessions": [ { "name": "my-project", "attached": false, "path": "/Users/dev/my-project", "windows": 1, "createdAt": "2024-01-15T10:30:00.000Z" } ], "connection": { "tailscaleDnsName": "laptop.tail1234.ts.net", "tailscaleSshCommand": "ssh laptop.tail1234.ts.net", "tailscaleReady": true, "hostname": "laptop" } } ``` Session objects have no `backend` field — every session in this API is a tmux session. ### Create workspace ```bash curl -X POST http://127.0.0.1:7788/v1/sessions \ -H "Authorization: Bearer $HACK_GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "agent-1", "cwd": "/path/to/project"}' ``` Request body: | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Workspace name (alphanumeric, dash, underscore) | | `cwd` | string | no | Working directory | Creation always uses tmux; there is no backend override. Response (201): ```json { "session": { "name": "agent-1", "attached": false, "path": "/path/to/project", "windows": 1, "createdAt": "2024-01-15T10:35:00.000Z" } } ``` ### Get workspace ```bash curl -H "Authorization: Bearer $HACK_GATEWAY_TOKEN" \ http://127.0.0.1:7788/v1/sessions/agent-1 ``` Response includes connection info for SSH access to the workspace: ```json { "session": { ... }, "connection": { "tailscaleDnsName": "laptop.tail1234.ts.net", "tailscaleSshCommand": "ssh laptop.tail1234.ts.net", "tailscaleReady": true, "hostname": "laptop" } } ``` ### Execute command ```bash curl -X POST http://127.0.0.1:7788/v1/sessions/agent-1/exec \ -H "Authorization: Bearer $HACK_GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"command": "bun test"}' ``` This sends the command followed by Enter to the session. Response: ```json { "status": "sent", "session": "agent-1" } ``` ### Send raw input ```bash curl -X POST http://127.0.0.1:7788/v1/sessions/agent-1/input \ -H "Authorization: Bearer $HACK_GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"keys": "C-c"}' ``` Send raw keystrokes without Enter. Useful for: - `C-c` (Ctrl+C) - `C-d` (Ctrl+D) - `Escape` - Arrow keys: `Up`, `Down`, `Left`, `Right` - `Tab` Note: - `keys` are passed directly to `tmux send-keys` — there is no zellij branch on this endpoint. Response: ```json { "status": "sent", "session": "agent-1" } ``` ### Stop workspace ```bash curl -X POST -H "Authorization: Bearer $HACK_GATEWAY_TOKEN" \ http://127.0.0.1:7788/v1/sessions/agent-1/stop ``` Response: ```json { "status": "stopped", "session": "agent-1" } ``` ### Error codes | Status | Error | Description | | --- | --- | --- | | 400 | `invalid_json` | Request body is not valid JSON | | 400 | `missing_name` | Workspace name not provided | | 400 | `invalid_name` | Workspace name contains invalid characters | | 400 | `missing_command` | Command not provided for exec | | 400 | `missing_keys` | Keys not provided for input | | 400 | `missing_session_id` | Session ID not in URL | | 404 | `session_not_found` | Session doesn't exist | | 409 | `session_exists` | Session already exists (on create) | | 500 | `create_failed` | Session create failed | | 500 | `stop_failed` | Session kill failed | | 500 | `exec_failed` | Session exec failed | | 500 | `input_failed` | Session input failed | ## Example: Remote agent workflow > **Status: unsupported experimental.** Like the Daemon sessions API above, this workflow depends > on the gateway, which is outside the supported v3 product contract and hidden behind > `hack help --all`. `gateway.example.com` below stands in for whatever tunnel/ingress you put in > front of the gateway yourself (Cloudflare, Tailscale, SSH port-forward) — hack does not provide > that ingress. 1. **Create a workspace from your remote client**: ```bash curl -X POST http://gateway.example.com/v1/sessions \ -H "Authorization: Bearer $TOKEN" \ -d '{"name": "agent-task-1", "cwd": "/home/dev/project"}' ``` 2. **Execute commands**: ```bash curl -X POST http://gateway.example.com/v1/sessions/agent-task-1/exec \ -H "Authorization: Bearer $TOKEN" \ -d '{"command": "git pull && bun install && bun test"}' ``` 3. **SSH in to check progress**: ```bash ssh laptop.tail1234.ts.net -t "hack session attach agent-task-1" ``` 4. **Clean up when done**: ```bash curl -X POST http://gateway.example.com/v1/sessions/agent-task-1/stop \ -H "Authorization: Bearer $TOKEN" ``` --- # Development runtime safety Hack's development defaults must preserve hot reload without manufacturing host-wide CPU, memory, or storage pressure. This guide records the runtime contract added after the 2026-07-30 OrbStack/Next investigation. ## Root cause and scope The generated Compose path used to inject both `CHOKIDAR_USEPOLLING=true` and `WATCHPACK_POLLING=true` into every discovered service. The injection was unconditional: Next and non-Next services, small repos and bind-mounted monorepos, and runtimes with working native file notifications all received the same polling policy. In the observed Apple Silicon workload, polling combined with an unnecessary `linux/amd64` override and multi-gigabyte persistent `.next` volumes. The result was sustained emulation/watcher CPU, very high cumulative writes, and a large set of stopped branch containers and cache volumes. The bounded comparison removed those project-level overrides without changing application code: the Next 16 development server ran as native arm64, hot reload remained available through native notifications, and idle container CPU settled near zero. A slow-filesystem warning remained for the `.next` volume, which identifies storage as a latency factor but does not justify forcing polling. Hack owns only two parts of that failure chain: 1. generated Compose defaults must not impose polling; 2. lifecycle targeting and optional cache cleanup must not silently abandon branch-owned resources. Image platform selection and application health-check cadence remain explicit project configuration. Hack does not rewrite them. ## File watching contract Generated services rely on the container runtime's native file notifications. Hack does not add Chokidar or Watchpack polling variables. A project can still set either variable on an individual service when a measured runtime requires it: ```yaml services: web: environment: CHOKIDAR_USEPOLLING: "true" WATCHPACK_POLLING: "750" ``` Keep polling service-scoped and measure CPU plus write activity after enabling it. Next's local development guidance notes that Docker filesystem access on macOS and Windows can be slower than host-local development; OrbStack likewise documents that bind mounts cross the macOS boundary while named volumes remain on the Linux side. Those storage tradeoffs are separate from watcher correctness. ## Branch rename contract An implicit `hack down` in a linked worktree uses Docker's Compose labels and exact `com.docker.compose.project.working_dir` evidence: - no owned runtime: use the current sanitized Git branch, preserving normal behavior; - exactly one owned runtime: target it, including when it is Created or stopped and the Git branch now has a different name; - multiple owned runtimes: fail before Compose mutation and require `--branch `; - explicit `--branch`: always target that branch without inference; - detached HEAD: continue to require an explicit branch; - primary checkout: continue to target the base instance by default. Canonical checkout paths prevent one worktree from stopping a sibling's runtime. ## Disposable cache cleanup contract Ordinary `hack down` preserves every named volume. `hack down --prune-caches` is a narrow, confirmation-gated addition for build caches. It removes only exact volume names observed on the target containers, then independently verifies the volume's Compose project and logical-volume labels. A named volume is eligible when every observed destination ends in `.next`, or when the top-level Compose volume explicitly declares `hack.cache.disposable: "true"`. The explicit label makes cleanup framework-agnostic without guessing whether destinations such as `.turbo`, `target`, `.gradle`, or a generic `cache` directory contain disposable output: ```yaml services: worker: volumes: - build-cache:/app/target volumes: build-cache: labels: hack.cache.disposable: "true" ``` Runtime inventory mount records now expose the Docker volume `name` separately from `source` (the engine storage path), so callers can inspect the same removal identity without guessing. The cleanup intentionally excludes: - bind mounts; - database, Redis, application-upload, and dependency volumes unless explicitly marked disposable; - external or unlabeled non-Next volumes; - volumes owned by another Compose project or checkout; - loose volume-name matches. Use `--yes` only after reviewing the same project-scoped evidence. Hack does not infer disposability from arbitrary volume names or mount paths because those signals cannot distinguish generated build output from durable application data. ## Doctor boundary This change does not add threshold-based doctor warnings for CPU, stopped-instance counts, volume size, or `linux/amd64` on arm64. Those signals can be legitimate and require image/platform or workload context that static configuration cannot prove. The generated-polling regression tests, branch-ownership resolution, and confirmation-gated cleanup are deterministic; policy guesses are left out of doctor until Hack can make them equally actionable. ## References - [Next.js local development](https://nextjs.org/docs/app/guides/local-development) - [Next.js Turbopack reference](https://nextjs.org/docs/app/api-reference/turbopack) - [OrbStack volumes and mounts](https://docs.orbstack.dev/docker/file-sharing) - [OrbStack efficiency](https://docs.orbstack.dev/efficiency) --- # Agent-first setup Let a coding agent (Claude Code, Codex) stand a project up under hack — or adopt an existing setup — instead of doing it by hand. One canonical onboarding prompt drives every entry point below; the content lives in the CLI, so it is always current for your installed version. ## Entry points (same prompt, four ways in) | You have | Use | | --- | --- | | A fresh repo + an agent CLI installed | `hack init --with claude\|codex\|both` | | An existing hack project | `hack agent onboard` (prints the prompt) | | An agent session with the hack skills installed | `/hack-init` | | A no-shell MCP client | the `hack-init` MCP prompt (`hack setup mcp`) | ## New repo: `hack init --with` ```bash cd /path/to/repo hack init --auto --with claude # or codex, or both ``` - Runs the normal init, then launches the agent CLI interactively with the full onboarding prompt (`claude ""` / `codex ""`). - If the agent CLI is not on PATH, the prompt is printed with copy-paste instructions instead. - If `.hack/` already exists, init is skipped and the handoff proceeds in existing-project mode. - With `--no-interactive` (or `HACK_NO_INTERACTIVE=1`, or no TTY) nothing is spawned — the prompt is always printed. ## Existing project: `hack agent onboard` ```bash hack agent onboard # prints the onboarding prompt for this project hack agent onboard | pbcopy # copy it for any agent session ``` The prompt picks up the project name and dev host from `.hack/hack.config.json` when present. ## Agent-side skill: `/hack-init` `hack setup claude` and `hack setup codex` install a thin `hack-init` skill (`.claude/skills/hack-init/SKILL.md` / `.codex/skills/hack-init/SKILL.md`). The skill tells the agent to run `hack agent onboard` (or fetch the `hack-init` MCP prompt) and follow it — the content stays in the CLI, so installed skills fetch guidance matching the installed CLI. ## What the prompt covers 1. Inventory — package manager, services, ports, backing services, and required variable names. Preserve the existing credential flow without copying secrets. 2. Setup — configure the requested container or host workflow, routing, and managed env injection. Verify replacements before removing any old env files. 3. Platform nuances — isolate Linux native dependencies from host installs when needed. Reuse existing tooling; add an ops container only when justified. 4. Running things — choose `hack run`, `hack exec`, or `hack host exec` for the target. 5. Verification — check expected long-running services, successful one-shot exits, actual routed URLs with normal TLS verification, and the requested host workflow. Preserve native trust approvals, ownership, and destructive-cleanup gates. Unrelated optional integration warnings do not block completion. ## Partial adoption (backing services only) Choose the shape that fits the requested workflow. Hack can run only the backing services — postgres, temporal, redis, and similar — while app dev servers (`bun dev`, `dotnet watch`, `vite`) stay on the host. **When to choose it:** - Heavy native toolchains (.NET, large native builds) develop faster on the host than in a container. - The hot-path dev server (the one you restart constantly) benefits from host-native file watching, debuggers, or hot reload that containers make slower or flakier. - The team already has a working host-based dev workflow and containerizing it is not worth the churn. **What hack still gives you in this shape:** - Stable hostnames and TLS for HTTP services and admin UIs via Caddy. Raw database protocols use their configured host ports or container-network addresses. - Branch instances for the containerized services (`--branch `), even though the host-run app processes are not branch-isolated on their own. - Managed environment configuration and secret injection. Commit only non-secret values; use the configured secret backend for credentials. - Lifecycle hooks (`startup`/`lifecycle` in `.hack/hack.config.json`) for host-side setup — tunnels, SSO bootstrap — that would otherwise be ad-hoc terminal steps. **How to wire it:** - Define only the backing services in `.hack/docker-compose.yml`; add Caddy labels where a stable hostname is useful (databases usually don't need one — connect by container port — but admin UIs, queues, or shared services often do). - Give host dev servers their connection info via `hack host exec --scope -- ` (injects the resolved env without touching `.env` files) without exposing secret values. - App URLs can stay on plain `localhost:` for the fast path, or get promoted to a routed compose service later if you want a stable `*.hack` hostname for them too — the two are not mutually exclusive and can be migrated incrementally. **Tradeoffs vs full containerization:** - No single `hack up` brings up the whole stack for a newcomer — they still need to start the host dev servers themselves (document that step). - Host-run app processes are not branch-isolated by hack; running two branches side by side means varying ports/env per worktree yourself. Partial adoption is a valid end state, not an incomplete migration — do not treat it as something to "finish" into full containerization unless the tradeoffs above actually bite. ## Copy-paste bootstrap Paste this into a fresh Claude Code / Codex session started at the repo root: ```text Set this repository up to run under the hack CLI. Run `hack agent onboard` (pass --no-interactive to hack commands) and follow the printed onboarding prompt for the requested scope. Preserve native approvals, existing credential flows, runtime ownership, and publishing gates. If the CLI is missing, report that blocker and continue independent setup work. Verify the chosen services and host workflows, and distinguish reachability from certificate trust. ``` --- # Local Helpers Hack no longer ships hosted or broker-backed integrations. What remains: - local env management and host/container injection - local sessions and runtime orchestration - optional coding-agent setup helpers: `hack init --with claude|codex|both`, `hack agent onboard` / `hack agent init` / `hack agent prime`, and scoped `hack setup` commands `hack setup sync` keeps interactive output compact: it summarizes each scope and only expands the path and reason for stale, missing, or failed artifacts. Exit status remains the automation contract, and the individual `hack setup cursor|claude|codex|agents|mcp --check` commands remain available when you need per-artifact detail. What does not ship: Hack Tickets, built-in GitHub or Linear integrations, hosted auth/account/org/team surfaces, and the web dashboard control plane. Recommended replacements: - GitHub: native `git` and `gh` - planning systems: keep them outside Hack and use the tracker selected by the project ## Agent integration freshness Hack maintains project instructions plus global Cursor, Claude, Codex, and shared `~/.ai/skills` surfaces. Generated guidance identifies the CLI version that rendered it. Run checks when Hack is relevant and there is evidence of drift; there is no mandatory startup audit. Plain `hack agent prime`, including installed SessionStart/PreCompact hooks, prints the current primer without scanning project or global integrations. Use `hack agent prime --check` for an explicit read-only inventory, or a targeted `hack setup ... --check` for per-artifact detail. Outside a Hack project, `hack agent prime --check` inventories global integrations only. Doctor classifies inventory commands as follow-up investigation and keeps scoped repair guidance separate. Inventory findings do not block unrelated work, and missing optional integrations need not be installed. Ordinary commands, `hack update`, and `hack doctor --fix` never modify these surfaces. Doctor may report freshness findings. Repair only the affected integration and scope covered by the request; `hack setup sync --all-scopes` remains available for an explicitly requested full refresh. Existing authorization for that action and scope is sufficient. After repair, read the updated guidance; restart only if the client cannot reload changed hooks or skills. ## Optional native plugins Hack also ships an optional native plugin for Codex, Claude Code, and Cursor in `plugins/hack`. It bundles the `hack-cli` and `hack-init` skills, the `hack mcp serve` server, Claude primer hooks, and a Cursor rule that the agent can select when relevant. Install the Hack CLI first and make `hack` available on the client's PATH. Choose either the native plugin or the existing standalone setup for each client. Installing a plugin does not migrate or remove existing rules, skills, hooks, or MCP configuration. If switching an existing installation, inspect the chosen client's project and user scopes, verify the plugin loads in a fresh session, then explicitly remove only duplicate Hack-owned entries in that scope. Preserve customized content. Do not use a broad sync or removal command as a migration step. If validation fails, disable the plugin and keep using the standalone integration. Codex: ```bash codex plugin marketplace add hack-dance/hack codex plugin add hack@hack-dance ``` Claude Code: ```bash claude plugin marketplace add hack-dance/hack --sparse .claude-plugin plugins/hack claude plugin install hack@hack-dance ``` Cursor: open Customize, find Hack in the configured marketplace, and choose the project or user installation scope. For local development, copy `plugins/hack` into `~/.cursor/plugins/local/hack` when that destination is unused, then reload Cursor and inspect the Hack plugin in Customize. Cursor 3.18.25 rejects symlinks to targets outside its local-plugin root; use a copy rather than weakening that boundary. See [Cursor's plugin documentation](https://cursor.com/docs/plugins) for marketplace and local development support in the installed client version. After installing, verify in a fresh session that both skills are discoverable, `hack mcp serve` can initialize, and the client's hooks or rule load. A successful marketplace registration alone does not prove those components loaded. The plugin's MCP fallback is for clients without shell access; use the Hack CLI when shell is available. Plugin updates are managed by the client. Ordinary Hack commands, `hack update`, `hack doctor --fix`, and primer hooks never install plugins or clean up integrations. A missing optional plugin is not a broken Hack installation. Model selection and global instruction, evolution, and writeback policy remain with the user's existing provider configuration; the Hack plugin adds only Hack-specific guidance. Contributors: change shared behavior in `src/agents/instruction-source.ts`, then run `bun run generate:agent-plugins`. The generation tests check bundled guidance against the canonical renderers. Release preparation regenerates all plugin content after the version bump and commits the complete bundle with the CLI release. --- # CLI Overview Hack is the local-first CLI surface. This page is a supported-surface overview with the running-things decision guide; for exhaustive per-command options and flags, see the generated [CLI reference](/docs/v4/reference/cli) (`bun run docs:cli-reference`, or `hack help ` in the terminal). ## Core commands - `hack init` — generate `.hack/` (compose + config); `--with claude|codex|both` also hands off to agent-assisted onboarding - `hack up` / `hack down` / `hack restart` - `hack open` — open/print the project URL - `hack logs` — tail logs (compose by default; Loki via `--loki`/`--query`) - `hack ps` / `hack status` — project status - `hack projects` — registry + running instances; `hack projects prune --project ` safely scopes stale registry/container cleanup to one project family (omit `--project` only for an intentional machine-wide prune) entries and stops orphaned containers - `hack env` — env values and local secrets - `hack host exec` / `hack host shell` — host commands/shells with Hack-resolved env injected - `hack run [cmd...]` — one-off command in a fresh service container - `hack exec -- ` — command in an already-running service container - `hack session` — persistent project workspaces (tmux-first) - `hack doctor` / `hack doctor --fix` — validate and repair local setup - `hack daemon` — optional local daemon for faster JSON status/ps - `hack agent onboard` — agent-assisted onboarding for existing projects - `hack setup` — install/refresh agent integrations (Cursor rules, Claude hooks, Codex skill, MCP) Interactive diagnostics use compact status rows: healthy groups stay on one line, while warnings and errors expand with wrapped detail and recovery guidance. `hack doctor --json` remains the stable, fully detailed automation surface. Generic macOS resolver setup is shown only when those resolver checks need attention. Run `hack help` for the full command list, or `hack help --all` to include hidden unsupported experimental commands. Every command and flag on this page is also in the generated [CLI reference](/docs/v4/reference/cli). Hosted auth/account/org/team, built-in GitHub and Linear workflows, and Hack Tickets are outside the CLI surface. Use native `git` and `gh` for repository collaboration. ## Unsupported experimental These commands remain source-available but are outside the supported v3 product contract. They are hidden from default `hack --help` (see `hack help --all`) and print a warning when invoked: - `hack remote` - `hack gateway` - `hack node` - `hack dispatch` See [Beta workflows](https://github.com/hack-dance/hack/blob/91b14bf0abb17602142425de0d02628a3e8a2615/docs/beta.md) for guides on this surface. ## Agent/scripted ergonomics - `--json` on `hack up`/`down`/`restart`/`doctor` emits a `{ok, data | error: {code, message}}` envelope with stable `E_*` error codes. `--json` on `hack up` implies `--detach`. - `--no-interactive` (or `HACK_NO_INTERACTIVE=1`) is a global flag: commands never prompt — they apply documented defaults or fail fast with `E_INTERACTIVE_REQUIRED`. - `NO_COLOR` (or `HACK_NO_COLOR`) disables colored/decorated output. Generated agent docs, Cursor rules, Codex skills, and the shared `~/.ai/skills/hack-cli` skill carry the Hack CLI version that generated them. Plain `hack agent prime` prints current guidance without an integration scan. Use `hack agent prime --check` for an explicit project/global inventory or a targeted `hack setup ... --check` when diagnosing drift. Missing optional integrations do not require installation. Repair only affected targets within the authorized scope; use `hack setup sync --all-scopes` for an explicitly requested full refresh. Read updated guidance after repair; restart only if the client cannot reload changed hooks or skills. Ordinary commands, `hack update`, and `hack doctor --fix` never render, repair, remove, or otherwise mutate integration files. CLI upgrades do not trigger integration updates. ## First-run path ```bash hack global install hack init hack up --detach hack open ``` Agent-assisted alternative for a new repo: `hack init --with claude|codex|both`. For an existing project without `.hack/`, use `hack agent onboard`. See [Agent-first setup](/docs/v4/guides/agent-first-setup). ### Browser URL preference `hack open` keeps `dev_host` as the primary routing identity, but automatically opens the OAuth alias (for example, `myapp.hack.gy`) when `oauth.enabled` is true. Service shorthand and branch instances follow the same preference, so `hack open api --branch feature-x` resolves the branch-qualified alias URL. Set `open.prefer` in `.hack/hack.config.json` to `auto` (the default), `alias`, or `dev`. Override one invocation with `hack open --prefer `. Explicit URLs and fully qualified host targets are preserved. Selecting `alias` without an enabled OAuth alias fails with recovery guidance instead of silently opening the dev host. For a custom `dev_host` outside Hack's managed `.hack` namespace, `auto` keeps the development host because Hack does not synthesize a Caddy alias route for it. ## Running things (decision guide) - One-off command in a fresh service container (deps started as needed): `hack run `. - Command inside an already-running service container: `hack exec -- `. - Host script that needs hack-stored env: `hack host exec --env --scope -- ` — this is the way to run repo scripts; never read `.env` files directly. - Interactive host shell with injected env: `hack host shell --env --scope `. - Browser/host URL: use `hack open --json`; OAuth aliases are preferred when enabled. - Container-to-container traffic: use Compose DNS rather than routing back through Caddy. Service-scoped runtime changes do not run project-wide lifecycle hooks and do not start Compose dependencies implicitly, except declared shared-cache installers needed by the selected services: ```bash hack up api worker --env qa --detach hack restart api --env qa hack env apply --service api --env qa ``` Full detached startup and inspection are bounded. A timeout returns `E_STARTUP_TIMEOUT`, terminates the Compose process group, and leaves an explicit repair path instead of hanging indefinitely. `hack doctor --fix` starts exact project containers left in `Created`; it never removes those containers as part of this repair. ## Dependency bootstrap integrity Hack detects package-manager install services by their command or by the explicit `hack.dependencies.bootstrap=true` Compose label. Before such a service can mutate the runtime, Hack scans `.npmrc`, `.yarnrc.yml`, and `bunfig.toml` for `${VAR}` credential references and fails with `E_ENV_KEY_MISSING` when the selected Hack overlay cannot supply them. Values are never printed. To share dependency data only across worktrees with the same lockfile/runtime fingerprint, label the bootstrap service with a logical top-level Compose volume: ```yaml services: install-workspace: command: bun install --frozen-lockfile labels: hack.dependencies.bootstrap: "true" hack.dependencies.cache-volume: workspace-dependencies hack.dependencies.lockfiles: bun.lock,package.json hack.dependencies.runtime-files: .mise.toml volumes: - workspace-dependencies:/app/node_modules ``` Hack generates a content-addressed volume name from the declared inputs. Branch instances adopt an existing compatible volume automatically; a lockfile or runtime change selects a new volume. No service name such as `deps` is special. `hack run` resolves the same cache as `up` and `restart`. Before a scoped `up`, `restart`, or a consumer `run`, Hack executes each needed cache installer with the same Compose files and selected service environment, using `run --rm --no-deps`. Installers must be idempotent and coordinate concurrent writers (for example, a lock and a ready marker inside the volume). They run even for warm caches so an empty or interrupted cache cannot be mistaken for a ready one. No project lifecycle hook or unrelated dependency is started. A failed installer leaves existing consumer containers untouched; JSON lifecycle commands return `E_DEPENDENCY_BOOTSTRAP_FAILED`. Automatic initialization has a ten-minute process deadline. `hack exec` continues to use the existing container and its mounted cache until that container is explicitly recreated. ## Branch instances and linked worktrees `--branch ` on `hack up/down/restart/ps/logs/open/run/exec` targets a separate branch instance (compose project `--`, hostnames prefixed with the branch). In a linked git worktree, these commands default the branch instance to the sanitized current git branch when no `--branch` is passed (`worktree.auto_branch`), so two checkouts never fight over the same hostnames. A one-line notice is printed to stderr when the default kicks in, so captured stdout stays clean. Before `up` or `restart`, Hack also checks for a non-terminal instance previously started from the same worktree. If the worktree's current branch would auto-target a different Compose project, Hack prints a warning naming both the existing and new targets. Pass `--branch ` to make the target explicit. `hack down` applies a stricter ownership rule. When a linked worktree's Git branch was renamed and that exact checkout owns one differently named Compose runtime, implicit down targets the existing runtime instead of succeeding against an empty newly derived name. Running, Created, and stopped containers all count as ownership evidence. If the checkout owns multiple runtimes, implicit down fails and lists them; pass `--branch ` to select one. Exact checkout paths and Compose project families are required, so Hack never retargets to a sibling checkout. A detached linked worktree has no branch name to derive, so these commands fail instead of silently targeting the base instance. Pass `--branch ` to select an isolated instance, or set `worktree.auto_branch` to `false` only when intentionally opting into the base instance. Opt out: - pass `--branch ` explicitly (always wins), or - set `worktree.auto_branch` to `false` in `.hack/hack.config.json` to target the base instance. The primary checkout is unchanged: no `--branch` means the base instance. ### Disposable cache volumes Compose preserves named volumes on ordinary `down`, which is correct for application data but can leave branch-specific build caches behind. Use the explicit cleanup path when those caches are disposable: ```bash hack down --prune-caches # scripted: hack down --prune-caches --yes --json ``` Hack recognizes named volumes mounted only at `.next` destinations as disposable Next build caches. For any other framework, library, or language, mark the top-level Compose volume explicitly: ```yaml services: web: volumes: - turbo-cache:/app/.turbo volumes: turbo-cache: labels: hack.cache.disposable: "true" ``` The label applies to the exact volume, so the same contract works for Rust `target`, Gradle, Go, Python, or other generated caches without relying on names or destination guesses. Hack snapshots mounts before stopping the target and considers a volume removable only when all of these are true: - the container belongs to the exact targeted Compose project and checkout; - the mount type is a named volume; - `docker volume inspect` independently reports both the exact `com.docker.compose.project` label and a `com.docker.compose.volume` label; - every observed destination's final path segment is `.next`, or the volume itself has `hack.cache.disposable=true`. Interactive cleanup shows the exact volumes, services, and destinations and defaults to “no.” `--json` and other scripted use must add `--yes`; otherwise the command fails before down with `E_INTERACTIVE_REQUIRED`. Postgres, Redis, dependency, application-data, bind, external, unlabeled non-Next, and sibling-checkout volumes do not satisfy the removal contract unless a project author deliberately marks that exact Compose volume disposable. Hack deliberately does not run broad `docker volume prune` or `docker compose down -v`. ### Runtime host metadata Containers started by `hack up`, `hack restart`, and `hack run` receive the effective instance and public-route metadata. This keeps server-generated links, OAuth callbacks, webhooks, and other browser-facing URLs isolated when the same project runs in multiple worktrees. | Variable | Value | | --- | --- | | `HACK_BRANCH` | Effective branch slug, or an empty string for the base instance | | `HACK_COMPOSE_PROJECT` | Effective Compose project name | | `HACK_DEV_HOST` / `HACK_DEV_URL` | Effective root development host and HTTPS URL | | `HACK_ALIAS_HOST` / `HACK_ALIAS_URL` | Effective OAuth alias host and URL, when enabled and routed by Hack | | `HACK_SERVICE_NAME` | Current Compose service name | | `HACK_SERVICE_URL` | Current service's first public URL, when routable | | `HACK_SERVICE_URLS` | JSON array of every public URL for the current service | | `HACK_RUNTIME_METADATA` | Versioned JSON document containing the instance hosts and every routable service's URL list | Example `HACK_RUNTIME_METADATA` for a branch instance: ```json { "version": 1, "branch": "feature-x", "composeProject": "demo--feature-x", "hosts": { "dev": "feature-x.demo.hack", "alias": "feature-x.demo.hack.gy" }, "services": { "web": { "urls": ["https://feature-x.demo.hack"] }, "api": { "urls": ["https://api.feature-x.demo.hack"] } } } ``` The service map is derived from effective Caddy routes; unroutable services are omitted. Use Compose DNS names such as `http://api:3000` for container-to-container traffic and runtime metadata only when a public/browser-reachable URL is required. Explicit project env values retain precedence over generated metadata for backward compatibility. Existing containers receive the contract after their next `hack up` or `hack restart`; `hack exec` observes the values already stored in the running container. Hack materializes this contract in generated, machine-local Compose overrides. Do not edit or commit `.hack/.internal/compose.runtime.override.yml` or `.hack/.branch/compose..runtime.override.yml`; runtime commands refresh them and Hack's managed `.hack/.gitignore` covers them. Linked worktrees also inherit the project secret key automatically from the primary checkout through the shared git common dir, so you don't need to copy `.hack.secret.key` by hand. Set `HACK_ENV_SECRET_KEY` for CI or fully detached environments. `hack doctor` flags divergent secret keys and `dev_host` collisions across checkouts. ## Environment model Canonical env files: - `.hack/hack.env.default.yaml` - `.hack/hack.env..yaml` - `.hack/hack.env.local.yaml` (worktree-local override) - `.hack/hack.env..local.yaml` (worktree-local override) Use `hack env add`, `hack env unset`, `hack env list`, and `hack env materialize` to manage them. Use `hack host exec` and `hack host shell` when you want Hack-resolved env injected into host-side commands. Use `--local` on env mutations when you want to write to the worktree-local override file instead of the shared repo file. `hack env materialize` is only for compatibility output. `hack doctor` will tell you when the materialized `.hack/.env` or `.hack/.env.state.json` is stale and should be regenerated. ## Project files Hack owns a committed `.hack/.gitignore` (self-healing on `init`/`up`) that ignores machine-local generated files (`.internal/`, `.branch/`, `.env`, `.env.state.json`, `hack.env*.local.yaml`). The retired `tickets/` path remains ignored only so upgrades cannot recommit legacy machine-local caches. Keep `.hack/.gitignore` committed. If generated files ever leak into git, `hack doctor --fix` untracks them (the files stay on disk). Runtime metadata is written to `.internal/compose.runtime.override.yml` for the base instance and `.branch/compose..runtime.override.yml` for branch instances. See [Architecture](/docs/v4/architecture) for the full file map. The global config root defaults to `~/.hack`; override it with `HACK_HOME`. ## Lifecycle Use `.hack/hack.config.json` `lifecycle` or `startup` for host-side setup instead of ad-hoc terminal tabs. For fixed-port helpers such as AWS SSM tunnels or local proxies, declare `singleton.ports`. Use `onConflict: "adopt"` only when an existing full listener set is equivalent and should be reused. Adoption does not transfer process ownership: `hack down` leaves adopted external listeners running. See [Lifecycle](/docs/v4/lifecycle) for the full model. --- # hack CLI reference Run `hack help ` for the same content in the terminal. Global options: `--no-interactive` (or `HACK_NO_INTERACTIVE=1`); JSON shapes vary by command and backend. See [Command and output contracts](/docs/v4/reference/command-contracts) for envelopes, bare reports and streamed records. ## Project ## `hack project` Inspect or manage project metadata ### Usage ```bash hack project [options] ``` ### Subcommands | Command | Summary | | --- | --- | | `hack project owner` | Inspect project ownership | ### Options | Option | Description | | --- | --- | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack project owner` Inspect project ownership ### Usage ```bash hack project owner [options] ``` ### Subcommands | Command | Summary | | --- | --- | | `hack project owner show` | Show the local ownership metadata for the current project | ### Options | Option | Description | | --- | --- | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack init` Initialize a repo (generate .hack/ with compose + config) ### Usage ```bash hack init [options] ``` ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--manual` | Skip discovery and define services manually (or generate a minimal compose in --auto) | | `--auto` | Run non-interactive init with sensible defaults | | `--name ` | Project slug (default: repo name) | | `--dev-host ` | DEV_HOST override | | `--oauth` | Enable OAuth-safe alias host | | `--oauth-tld ` | OAuth alias TLD override (default: gy) | | `--no-discovery` | Skip discovery and generate a minimal compose | | `--with ` | After init, hand the onboarding prompt to an agent CLI (prints the prompt when the CLI is missing or the run is non-interactive) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack up [services...]` Start project services (docker compose up) ### Usage ```bash hack up [services...] [options] ``` ### Arguments | Arg | Description | | --- | --- | | `services` | | ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--env ` | Apply an optional env overlay by name (use 'base' to bypass overlays) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--detach, -d` | Run in background (docker compose up -d) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--target ` | Execution target routing (auto routes to remote when project execution mode requires it) | | `--json` | Output JSON (machine-readable) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack down` Stop project services (docker compose down) ### Usage ```bash hack down [options] ``` ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--env ` | Apply an optional env overlay by name (use 'base' to bypass overlays) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--target ` | Execution target routing (auto routes to remote when project execution mode requires it) | | `--prune-caches` | After down, remove confirmed Compose-owned disposable cache volumes | | `--yes` | Confirm --prune-caches without prompting | | `--json` | Output JSON (machine-readable) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack restart [services...]` Restart the project or selected services ### Usage ```bash hack restart [services...] [options] ``` ### Arguments | Arg | Description | | --- | --- | | `services` | | ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--env ` | Apply an optional env overlay by name (use 'base' to bypass overlays) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--target ` | Execution target routing (auto routes to remote when project execution mode requires it) | | `--json` | Output JSON (machine-readable) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack ps` Show project status (docker compose ps) ### Usage ```bash hack ps [options] ``` ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--json` | Output JSON (machine-readable) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack tui` Open the project TUI (services + logs) ### Usage ```bash hack tui [options] ``` ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack run [cmd...]` Run a one-off command in a service container (docker compose run --rm) ### Usage ```bash hack run [cmd...] [options] ``` ### Arguments | Arg | Description | | --- | --- | | `service` | | | `cmd` | | ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--env ` | Apply an optional env overlay by name (use 'base' to bypass overlays) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--workdir ` | Working directory inside the container (docker compose run -w) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack exec [cmd...]` Run a command in an already-running service container (docker compose exec) ### Usage ```bash hack exec [cmd...] [options] ``` ### Arguments | Arg | Description | | --- | --- | | `service` | | | `cmd` | | ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--env ` | Apply an optional env overlay by name (use 'base' to bypass overlays) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--workdir ` | Working directory inside the container (docker compose run -w) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) | | `--help, -h` | Show help | | `--version, -v` | Show version | ## `hack logs [service]` Tail logs (compose by default; Loki for queries/history via --loki/--query) ### Usage ```bash hack logs [service] [options] ``` ### Arguments | Arg | Description | | --- | --- | | `service` | | ### Options | Option | Description | | --- | --- | | `--path, -p ` | Run a project command against a repo path (overrides cwd search) | | `--project ` | Target a registered project by name (from ~/.hack/projects.json) | | `--branch ` | Run against a branch-specific instance (compose name + hostnames) | | `--follow, -f` | Follow logs (default for `hack logs`) | | `--no-follow` | Print logs and exit (do not follow) | | `--tail ` | Tail last N log lines (default: 200) | | `--pretty` | Pretty-print logs (best-effort JSON parsing + formatting) | | `--json` | Output JSON (machine-readable) | | `--profile ` | Enable one or more compose profiles (comma-separated) | | `--compose` | Read logs directly from docker compose (bypass Loki) | | `--loki` | Force Loki backend (do not fall back to docker compose logs) | | `--services ` | Filter Loki logs by service(s), comma-separated (e.g. api,www) | | `--query ` | Raw LogQL selector/query (overrides auto selector built from project + services) | | `--since