Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
d111802
docs: add versioned agent-client capability matrix and policy controls
roodboi Oct 12, 2026
58c485a
fix(mcp): harden run argv and tool input contract
roodboi Oct 12, 2026
c6c0db3
fix(env): harden legacy env key files and AES-GCM envelope parsing
roodboi Oct 12, 2026
de862d7
Merge remote-tracking branch 'origin/next' into w14-rework
roodboi Oct 12, 2026
dc88c6b
Merge remote-tracking branch 'origin/next' into w06-rework
roodboi Oct 12, 2026
ffeecce
fix(agents): generate capability matrix doc and parse all guidance co…
roodboi Oct 12, 2026
56bac3a
fix(mcp): make typed refusals value-free and keep unknown-tool in-band
roodboi Oct 12, 2026
12e80af
chore: merge origin/next into wf/w15-w5-6
roodboi Oct 12, 2026
cd76b26
test(env): pin AES-GCM IV, size and tag-length guards with discrimina…
roodboi Oct 12, 2026
bcf88ec
fix(init): stop writing repo-local agent files by default
roodboi Oct 12, 2026
c005262
fix(env): resolve --env base and default the same way on every command
roodboi Oct 12, 2026
6136946
fix(agents): count corrupted and unreadable integrations as drift
roodboi Oct 12, 2026
9854d1b
test(env): pin the console logger backend for CLI refusal tests
roodboi Oct 12, 2026
6e1ff63
test(mcp): assert closed ambiguous_project refusal in project-name suite
roodboi Oct 12, 2026
1f8b51e
chore(x01): temporary witness cost trace for hosted measurement
roodboi Oct 12, 2026
0911e6d
perf(storage): coalesce witness helper work per native command
roodboi Oct 12, 2026
eb9a678
chore(x01): revert temporary witness cost trace
roodboi Oct 12, 2026
7b2ccfc
test(e2e): restore the networks scenario budget after witness coalescing
roodboi Oct 12, 2026
fa7741c
Merge remote-tracking branch 'origin/next' into wf/x03-witness-probes
roodboi Oct 12, 2026
37f093a
perf(storage): make witness engine and executable fences cheaper
roodboi Oct 12, 2026
6a4e118
chore(x03): temporary witness probe trace for hosted measurement
roodboi Oct 12, 2026
2b11ed3
fix(storage): keep closed probe cancellation for direct engine-ID fences
roodboi Oct 12, 2026
a777bc4
test(storage): pin witness fence counts and refusal controls
roodboi Oct 12, 2026
994ddb8
chore(x03): revert temporary witness probe trace
roodboi Oct 12, 2026
3edbd54
feat(agents): package offline skill references and the HACK-P pattern…
roodboi Oct 12, 2026
726cc27
test(e2e): add agent-surface-freeze negative control for guidance writes
roodboi Oct 12, 2026
9b971fa
ci: add gated Bun crash experiment and read-only crash census
roodboi Oct 12, 2026
1342871
perf(storage): read engine identity directly for docker context selec…
roodboi Oct 12, 2026
c2ecf9c
chore(x04): temporary witness probe trace for hosted measurement
roodboi Oct 12, 2026
5cf9f0f
chore(x04): revert temporary witness probe trace
roodboi Oct 12, 2026
ebe0539
test(agents): cover reference generation and reference-style and HTML…
roodboi Oct 12, 2026
6b54980
test: keep the Bun crash loop tests free of real crash reports
roodboi Oct 12, 2026
7a05b84
Merge branch 'next' into wf/w15-w5-6
roodboi Oct 12, 2026
c00468c
Merge branch 'next' into wf/w06-w5-3
roodboi Oct 12, 2026
e9b1252
Merge branch 'next' into wf/w13-w5b-2
roodboi Oct 12, 2026
af786b5
Merge branch 'next' into wf/w10-w5b-0
roodboi Oct 12, 2026
9db4dba
Merge branch 'next' into wf/w14-w5-5
roodboi Oct 12, 2026
5a731cc
Merge remote-tracking branch 'origin/wf/w06-w5-3' into train/queue-pr…
roodboi Oct 12, 2026
38d9d7f
Merge remote-tracking branch 'origin/wf/w14-w5-5' into train/queue-pr…
roodboi Oct 12, 2026
d50e935
Merge remote-tracking branch 'origin/wf/w15-w5-6' into train/queue-pr…
roodboi Oct 12, 2026
8bab674
Merge remote-tracking branch 'origin/wf/w10-w5b-0' into train/queue-p…
roodboi Oct 12, 2026
26958c9
Merge remote-tracking branch 'origin/wf/w13-w5b-2' into train/queue-p…
roodboi Oct 12, 2026
07a1834
Merge remote-tracking branch 'origin/wf/w07-w6-3' into train/queue-pr…
roodboi Oct 12, 2026
a92e3ab
Merge remote-tracking branch 'origin/wf/w02-w6-1' into train/queue-pr…
roodboi Oct 12, 2026
3e261d5
Merge remote-tracking branch 'origin/wf/w05-w6-2' into train/queue-pr…
roodboi Oct 12, 2026
6b22e00
test(patterns): add validated HACK-P01 and HACK-P02 example fixtures
roodboi Oct 12, 2026
b07121f
feat(exec): warn value-free when a running service's env changed sinc…
roodboi Oct 12, 2026
9f20366
fix(env): write .hack/.env and the encrypted store atomically with 0600
roodboi Oct 12, 2026
42e7b3b
fix(mcp): separate log cutoffs from backend failures
roodboi Oct 12, 2026
e2c41a9
fix(mcp): align budget docs with log cutoffs and keep byte-cut failures
roodboi Oct 12, 2026
cea44f8
feat(mcp): add exec, doctor, env explain/keys and instances tools
roodboi Oct 12, 2026
85c67b0
fix(ci): allowlist the synthetic MCP inspect-tool canary for gitleaks
roodboi Oct 12, 2026
10b6d73
fix(agents): point the env matrix bullet at the offline env guide
roodboi Oct 12, 2026
77da332
Merge remote-tracking branch 'origin/wf/w13-w5b-2' into train/queue-p…
roodboi Oct 12, 2026
4e3acee
test(exec): cover failed targeted restart env digest handling
roodboi Oct 12, 2026
cb665d5
fix(env): scope the env-file symlink refusal doc to write paths
roodboi Oct 12, 2026
3a6f73c
docs(mcp): clarify which instances hack.project.instances lists
roodboi Oct 12, 2026
c2b41ff
chore: merge train/queue-preview into wf/y3-envfile-b6
roodboi Oct 12, 2026
d9a5627
chore: merge train/queue-preview into wf/y1-logs-b6
roodboi Oct 12, 2026
09b4e83
Merge remote-tracking branch 'origin/train/queue-preview' into wf/y4-…
roodboi Oct 12, 2026
5afaf19
Merge remote-tracking branch 'origin/train/queue-preview' into wf/y5-…
roodboi Oct 12, 2026
36cd272
test(e2e): keep Hack diagnostics out of exec-stale-env probe output
roodboi Oct 12, 2026
b9ae28c
chore: merge wf/y1-logs-b6 into wf/y2-mcptools-b6
roodboi Oct 12, 2026
919de8b
test(e2e): reproduce dependency config-hash drift for hack run/exec
roodboi Oct 12, 2026
5078855
feat(doctor): add value-free env key strength diagnostic
roodboi Oct 12, 2026
5f8cb79
fix(logger): pass messages to gum after an end-of-options marker
roodboi Oct 12, 2026
0a26c64
Merge branch 'next' into wf/c5-confighash-b7
roodboi Oct 12, 2026
07413d0
Merge branch 'next' into wf/c4-keydiag-b7
roodboi Oct 12, 2026
6a043c0
Merge branch 'next' into wf/c6-gum-dash-b7
roodboi Oct 12, 2026
13165f9
Merge branch 'next' into wf/y4-patterns-b6
roodboi Oct 12, 2026
92c2ee1
Merge branch 'next' into wf/y5-staleenv-b6
roodboi Oct 12, 2026
4243d60
Merge branch 'next' into wf/y3-envfile-b6
roodboi Oct 12, 2026
c044445
Merge branch 'next' into wf/y1-logs-b6
roodboi Oct 12, 2026
6dadbe5
Merge branch 'wf/y1-logs-b6' into wf/y2-mcptools-b6
roodboi Oct 12, 2026
989fcea
Merge branch 'wf/c6-gum-dash-b7' into wf/y3-envfile-b6
roodboi Oct 12, 2026
c6055dd
Merge branch 'wf/y3-envfile-b6' into wf/c4-keydiag-b7
roodboi Oct 12, 2026
6a754df
Merge branch 'wf/c4-keydiag-b7' into wf/y4-patterns-b6
roodboi Oct 12, 2026
ef04f02
Merge branch 'wf/y4-patterns-b6' into wf/c5-confighash-b7
roodboi Oct 12, 2026
b0cde14
Merge branch 'wf/c5-confighash-b7' into wf/y5-staleenv-b6
roodboi Oct 12, 2026
5a258e6
Merge branch 'wf/y5-staleenv-b6' into wf/y1-logs-b6
roodboi Oct 12, 2026
642775e
Merge branch 'wf/y1-logs-b6' into wf/y2-mcptools-b6
roodboi Oct 12, 2026
effbb41
Merge branch 'next' into wf/y2-mcptools-b6
roodboi Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ condition = "AND"
paths = ['''^packages/runtime-core/tests/stream_relay_contract\.rs$''']
regexTarget = "line"
regexes = ['''^\s*let token = "0123456789abcdef0123456789abcdef";\s*$''']

[[rules.allowlists]]
description = "MCP inspect-tool tests use a fixed synthetic canary to prove values are never echoed"
condition = "AND"
paths = ['''^tests/mcp-inspect-tools\.test\.ts$''']
regexTarget = "line"
regexes = ['''(?m)^\s*const SECRET_CANARY = "zqx-secret-canary-7731";\s*$''']
3 changes: 3 additions & 0 deletions docs/guides/agent-first-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ but reaching it is a cutoff: the result is `ok: true` with `outputTruncated: tru
full output is needed. The budget bounds captured bytes, not total process memory
or JSON response size.

The [MCP tool contract](../reference/mcp-tools.md) lists every tool with its
annotations, inputs, the CLI command it runs and its result shape.

Each MCP session captures its launch working directory and environment. Project
registry selection, command execution and audit-file locations use that snapshot;
later sessions do not replace the state of an existing session. The standard stdio
Expand Down
1 change: 1 addition & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@
- [Extensions](./extensions.md)
- [Creating an extension](./guides/create-extension.md)
- [Integrations](./integrations.md)
- [MCP tool contract](./reference/mcp-tools.md) — every MCP tool: annotations, inputs, argv, results, and the value-free env tools
- [MCP logs contract](./reference/mcp-logs.md) — cutoff versus backend failure for the MCP log tools, and the cursor/handle decision
202 changes: 202 additions & 0 deletions docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# MCP tool contract

`hack mcp serve` exposes the tools below to MCP clients that have no shell. Each
tool runs one `hack` CLI command as a child process, with the session's captured
working directory and environment, and `HACK_NO_INTERACTIVE=1`. When a shell is
available, run the CLI command directly instead.

This page is the contract for tool names, annotations, inputs, the argv each tool
runs, and result shapes. The log tools have their own page:
[MCP logs contract](./mcp-logs.md).

## Tools

The annotation class is published in `tools/list`:

- `read-only`: `readOnlyHint: true`. The tool only inspects state.
- `additive`: `readOnlyHint: false`, `destructiveHint: false`. The tool starts
or restarts local runtime state but does not remove it.
- `destructive`: `readOnlyHint: false`, `destructiveHint: true`. The tool can
stop, replace or overwrite local state, or run an arbitrary command.

Every tool sets `openWorldHint: false`. Mutating tools set `idempotentHint: false`.

| Tool | Class | CLI command |
| --- | --- | --- |
| `hack.projects.list` | read-only | `hack projects --json` |
| `hack.project.status` | read-only | `hack ps --json` |
| `hack.project.up` | additive | `hack up --detach` |
| `hack.project.down` | destructive | `hack down` |
| `hack.project.restart` | additive | `hack restart` |
| `hack.project.run` | destructive | `hack run -- <service> [cmd...]` |
| `hack.project.logs.snapshot` | read-only | `hack logs --json --no-follow` |
| `hack.project.logs.tail` | read-only | `hack logs --json` |
| `hack.project.open` | read-only | `hack open --json` (never launches a browser) |
| `hack.project.init` | destructive | `hack init --auto` |
| `hack.project.exec` | destructive | `hack exec -- <service> <cmd...>` |
| `hack.project.doctor` | read-only | `hack doctor --json` |
| `hack.project.env.explain` | read-only | `hack env explain --json -- <key>` |
| `hack.project.env.keys` | read-only | `hack env list --json` |
| `hack.project.instances` | read-only | `hack projects --json --project=<name>` |

## Project selection

Project-scoped tools accept at most one of `projectName`, `repoRoot` or `path`.
With none, the session's working directory selects the project.

- `projectName` must name a registered project (`~/.hack/projects.json`).
- `repoRoot` and `path` must exist and lie inside a registered project's repo
root or project directory.
- With no selection, the working directory must contain a hack project or lie
inside a registered one.

`hack.project.doctor` passes a named project to the CLI as `--path=<repo root>`,
because `hack doctor` has no `--project` option.

`hack.project.instances` needs a registered project name, because
`hack projects` selects by name only. A path, or the working directory when
nothing is selected, maps to the registered project whose repo root or project
directory contains it. When registrations are nested, the deepest one wins. Two
different registrations at the same root are refused as `ambiguous_project`. A
project that has a `.hack/` directory but no registration is refused; run
`hack projects` or any project command in it to register it.

## Inputs and identifiers

Inputs are validated before any child process starts. Unknown input fields are
dropped, so they never reach the CLI argv.

- `service`, `services`, `profiles`, open `target`, and the env tools' `env` and
`service` must match `^[A-Za-z0-9][A-Za-z0-9_.-]*$`.
- `branch` must match `^[A-Za-z0-9][A-Za-z0-9_./-]*$`.
- Env `key` must match `^[A-Za-z_][A-Za-z0-9_]*$`. Scoped `service.KEY` syntax is
not accepted. Use the `service` input for the scope.
- Identifiers are at most 253 characters. None of these patterns can start with
`-`, so no identifier can be read as an option.
- Numeric limits are positive integers. A value above the ceiling is refused,
not clamped: `tail` and `maxEvents` up to 10,000, `maxMs` up to 60,000, and
`timeoutMs` up to 600,000.

Option values are always fused (`--branch=<value>`). Positional values that come
from the caller follow `--`, so the CLI parser ends option parsing before them.

## Results

Every result has the same structured content:

| Field | Meaning |
| --- | --- |
| `ok` | The child exited 0 (for the log tools, see the logs contract) |
| `command` | The argv that ran, or `""` when refused before spawn |
| `exitCode` | The child's exit code (1 for a refusal) |
| `stdout`, `stderr` | Captured child output (withheld for the env tools; `stdout` withheld for instances) |
| `data` | Parsed or projected result, when the tool defines one |
| `outputTruncated` | `true` when output reached the 8 MiB capture limit |
| `spawned` | `false` only on a refusal: no child, no audit line |
| `error` | Closed refusal or backend error: `code`, `reason`, `message` |

When `ok` is `false`, the result also has `isError: true`. Output above the
8 MiB capture limit stops the command, and `data` is omitted so that a prefix is
never presented as a complete result. The log tools are the exception, as their
contract describes.

### Refusals

A refusal happens before any child process exists. Its `error.reason` is one of
`invalid_arguments`, `conflicting_project_selection`, `invalid_project_name`,
`ambiguous_project`, `project_not_found`, `path_not_found`,
`path_not_registered`, `no_project_context` and `init_path_required`. The code is
`E_USAGE` or `E_PROJECT_NOT_FOUND`. The message is fixed text. It never repeats
caller input or registry values. For `invalid_arguments`, the message names the
rejected input fields, using only the tool's own schema keys.

## `hack.project.exec`

Runs a command in a service container that is already running (`docker compose
exec`). `hack.project.run` starts a new one-off container instead. Exec never
starts a container, and the CLI fails when the service is not running.

- Required: `service` and `cmd`, with at least one entry.
- Optional: project selection, `branch`, `profiles`, `workdir` and `timeoutMs`
(default 120,000).
- Argv: `hack exec [--project=…|--path=…] [--branch=…] [--profile=…]
[--workdir=…] -- <service> <cmd...>`. Entries in `cmd` such as `--help` or
`--branch` reach the container command and are never parsed by hack.
- `data.limits` echoes `{ timeoutMs, outputBytes }`. `outputBytes` is the shared
8 MiB capture limit.

## `hack.project.doctor`

Runs read-only setup diagnostics: `hack doctor [--path=…] [--branch=…] --json`.
The argv set is closed. `--fix`, `--migrate-env-config`, `--domain-migration`,
`--native-cleanup`, `--native-run-mapping`, browser observations and legacy
rebind acceptance are never passed. The CLI also refuses `--json` together with
`--fix`.

`data` is the CLI's JSON result envelope, with `data.checks`, `data.summary` and
`data.counts`. When any check reports an error, the CLI exits 1, so `ok` is
`false` while `data` still carries the full report.

## `hack.project.env.explain`

Explains one env key without its value:
`hack env explain [--project=…|--path=…] [--env=…] [--service=…] [--target=…]
--json -- <key>`.

- Required: `key`. Optional: project selection, `env` (overlay; `base` or
`default` selects base layers only), `service` (scope; default `global`) and
`target` (`host` or `compose`).
- `data` is an allowlisted projection: `key`, `available`, `secret`, `format`,
`environment`, `requested_scope`, `effective_scope`, `target`,
`delivered_to`, `source` and `precedence`. Any other field in the CLI output is
dropped.

## `hack.project.env.keys`

Lists env key names with metadata: `hack env list [--project=…|--path=…]
[--env=…] [--service=…] --json`. `--show-secrets` is never passed, and there is
no input for it.

`data` is `{ format, environment, selected_scope, scopes, keys,
missing_required }`. Each entry in `keys` is `{ key, secret, required, source,
resolved_from, services }`. `secret` is `true` when the CLI reports the key as
`keychain`-backed. Row values, materialized values, storage locations and
warning text are dropped. A row whose key does not match the env key pattern is
dropped whole.

### Value-free output

`hack env list --json` prints plaintext values, and CLI errors can quote env
files. Both env tools therefore withhold the child's output:

- `stdout` is always `""`.
- `stderr` is `""` on success. On failure it is a fixed message that tells you
to run the same `hack env` command in a terminal.
- `data` carries only the allowlisted fields above. It is `null` when the CLI
printed no parseable JSON object.

`command` still shows the argv, which contains the key name but no values. The
MCP audit log records the same argv.

## `hack.project.instances`

Lists a registered project's runtime instances: `hack projects --json
--project=<name>`. It reads the project's `runtime` and `branch_runtime`
entries, so it lists only instances that currently have containers, running or
stopped. A configured branch that was never started is not listed.

`data` is `{ project, found, runtime_ok, instances }`. `instances` lists the
default instance first, when it has containers, as `branch: null`. Each branch
instance follows, in CLI order. Each instance is `{ branch, compose_project,
working_dir, services }`, and each service is `{ service, containers, running }`
with container counts. Both counts include lifecycle-process containers, which
the CLI adds to the service with the process name. `stdout` is always `""`: the raw listing can cover other
projects and carries container labels, mounts and networks, which `data` omits.
`stderr` is returned as captured. `found` is `false` when the project is
registered but the CLI listing does not include it.

## Audit log

Every spawned command appends one JSON line to `$HACK_HOME/mcp-audit.log` (by
default `~/.hack/mcp-audit.log`): `ts`, `tool`, `command` and `exit_code`.
Refusals write nothing.
Loading
Loading