# The generated .hs-x directory.

HS-X keeps one machine-written directory inside your project: .hs-x/. Codegen rewrites the capability manifest and the typed refs stubs there on every dev and deploy run; the deploy adds its Cloudflare entrypoints, the resource plan, and a handful of local state files beside them. Exactly one of those files belongs in version control. This page is the contract for all of them: the real shape of each file, which command writes it, who reads it back, and how linked and unlinked projects differ.

## The 30-second answer

`.hs-x/` is the CLI's working directory inside your project. Three kinds of files live there: codegen output (`manifest.json`, `refs.d.ts`, `refs.js`), deploy artifacts (`alchemy.run.ts` and the `cloudflare/` build directory), and per-machine state (`project.json`, `cloudflare.json`, `hubspot.json`, `last-invocation.json`, and the MCP dev-session pair). **Every file under `.hs-x/` is machine-written; hand edits are overwritten on the next `hs-x dev` or `hs-x deploy`.**

The directory does not exist until the first `dev` or `deploy` creates it, and the scaffolded `.gitignore` ignores all of it except `.hs-x/alchemy.run.ts`: the checked-in record of the Cloudflare resources your deploys manage, kept so the project stays deployable without HS-X.

**If you only read one thing**

**Commit `.hs-x/alchemy.run.ts`; let everything else regenerate.** `hs-x dev` and `hs-x deploy` rewrite the manifest and refs stubs on every run, the deploy rewrites its own artifacts, and the state files are per-machine. Deleting the directory loses nothing a deploy can't rebuild, except local deploy state (`cloudflare.json`, `hubspot.json`) that makes repeat deploys land on the same resources.

## Every file, one table

| File | Written by | Read by |
| --- | --- | --- |
| `manifest.json` | `hs-x dev` (startup), `hs-x deploy` (every run, including `--plan`) | Deploy planning (it is the body of the plan request), `hs-x dev invoke` capability routing, `hs-x link` |
| `refs.d.ts`, `refs.js` | Same two commands, same runs | Your card code: typed dispatch routes for card backends, ids for app objects and app events |
| `alchemy.run.ts` | `hs-x deploy`, once a deploy plan exists | You. Nothing in the CLI reads it back; it is the leaveability record (§04) |
| `cloudflare/<worker>.entry.ts` | `hs-x deploy` (Cloudflare push) | The wrangler bundle: it is the deployed Worker's `main` |
| `cloudflare/<worker>.wrangler.toml` | `hs-x deploy` (Cloudflare push) | wrangler; `hs-x dev invoke --remote` reads the script name from it |
| `cloudflare.json` | First Cloudflare deploy; `hs-x link` rewrites the owner | Repeat deploys (account-id fallback), `hs-x flags`, unlinked `hs-x rollback`, `hs-x link`, `hs-x unlink` |
| `project.json` | `hs-x init` (when logged in), `hs-x link`, `hs-x deploy` | `hs-x status` / `logs` / `routes` / `drift` default ids; `deploy` and `dev` account preference |
| `hubspot.json` | `hs-x deploy`, after a successful HubSpot upload | The next deploy's app-id auto-reuse |
| `last-invocation.json` | `hs-x dev invoke` | `hs-x dev invoke --last` |
| `dev-session.json`, `dev-server.log` | The MCP server's dev-session tools | The MCP server's status, stop, and log-tail tools |

The split matters more than the count. The first two rows are **codegen output**, identical in linked and unlinked projects and rewritten wholesale on every run. `alchemy.run.ts` and the `cloudflare/` directory are **deploy artifacts**, derived from the active deploy plan. Everything else is **per-machine state**: small JSON files that let the next command on this checkout pick up where the last one stopped.

## manifest.json — the project's capability inventory

`manifest.json` is the serialized inventory of everything your `hsx.config.ts` and worker files declare: every worker, every capability with its kind (`tool`, `card-backend`, `trigger`, or `sync`), and any app objects, app-object associations, and app events. A `generatedAt` timestamp is the only metadata.

```json
{
  "generatedAt": "2026-06-11T14:01:38.000Z",
  "workers": [
    {
      "name": "email-guard",
      "capabilities": [
        {
          "kind": "tool",
          "id": "validate-email",
          "label": "Validate email",
          "objectType": "contact",
          "input": { "email": { "type": "string" } },
          "output": { "ok": { "type": "boolean" } },
          "runtimeNeeds": ["worker"]
        },
        {
          "kind": "card-backend",
          "id": "email-health",
          "runtimeNeeds": ["worker"]
        }
      ]
    }
  ]
}
```

Top-level keys beyond `workers` appear only when declared: `appObjects`, `appObjectAssociations`, `appEvents`. Each capability carries the fields relevant to its kind (`schedule` and `into` for syncs, `eventType` for triggers, `input`/`output` for tools), plus `runtimeNeeds`, the list that drives resource provisioning in §04.

Three consumers make this file load-bearing rather than informational:

1. A linked deploy sends the manifest as the body of the deploy-plan request, and the plan's `manifestHash` is computed over it. The deployed Worker attests that same hash back, which is how [drift detection](/docs/deploy-lifecycle) can tell whether running code matches the recorded revision.
2. `hs-x dev invoke` resolves which worker serves a capability by reading the manifest, and refuses ids it can't find there.
3. `hs-x link` falls back to the manifest when resolving the project name for control-plane registration.

## refs.d.ts and refs.js — typed handles for card code

A UI-extension card runs in HubSpot's sandboxed iframe and must never import your worker module: anything a worker file pulls in (connectors, secrets handling, Node dependencies) would ride into the card bundle. The refs stubs are the bridge. For every `card-backend` capability, codegen emits one frozen export carrying the capability id and its dispatch route, and nothing else:

```ts
// .hs-x/refs.d.ts — generated by hs-x. Do not edit.
export declare const emailHealth: {
  readonly id: "email-health";
  readonly kind: "card-backend";
  readonly dispatch: { readonly method: "POST"; readonly path: "/_hsx/cards/email-health" };
};
```

`refs.js` is the matching runtime module with the same exports as `Object.freeze`d values. Kebab-case ids become camelCase identifiers (`email-health` becomes `emailHealth`), and the dispatch path URL-encodes the id. Card code imports the ref by relative path (from `src/app/cards/`, that is `../../../.hs-x/refs.js`) and builds its `hubspot.fetch` call from it instead of hand-writing the route string:

```tsx
import { emailHealth } from "../../../.hs-x/refs.js";

const verdict = await hubspot
  .fetch(`${WORKER}${emailHealth.dispatch.path}`, {
    method: emailHealth.dispatch.method,
    body: { portalId, objectTypeId, objectId },
  })
  .then((response) => response.json());
```

**A refs stub carries an id and a route, never handler code, so importing one cannot leak Worker code into the card bundle.** The type contract is deliberately thin: the stubs do not carry your handler's payload or return types today, only the literal id, kind, and dispatch route, which is enough to make a misspelled backend id a compile error instead of a runtime `404 unknown_card_backend`.

Two more export families ride in the same files. Each declared app object emits `{ id, kind: "app-object", name }` and each app event emits `{ id, kind: "app-event", name }`, where `name` is the HubSpot-side object or event name; code that references them gets the same typo protection. A project with no card backends, app objects, or app events still gets both files, containing a bare `export {}`.

## alchemy.run.ts — the Cloudflare leaveability artifact

`alchemy.run.ts` is written during `hs-x deploy` as soon as a deploy plan exists (including a `--plan` dry run, once a project id is in play). It records the plan's identity and the full set of Cloudflare resources the deploy manages:

```ts
// Generated by hs-x. Do not edit by hand.
// This file is checked in as the Cloudflare leaveability artifact.

export const hsXAccountId = "acct_demo";
export const projectId = "email-guard";
export const deployId = "deploy_email-guard_007";

export const resources = [
  {
    "workerName": "hsx-acct-demo-email-guard-email-guard",
    "durableObjectNamespaces": [],
    "kvNamespaces": [],
    "d1Databases": [],
    "queues": [],
    "cronTriggers": []
  }
] as const;
```

Every name follows one scheme: `hsx-<account-slug>-<project>-<worker>`, with a suffix per resource type. Which resources appear is driven by the `runtimeNeeds` your capabilities declare:

| A capability needs | Resources planned for its worker |
| --- | --- |
| `durable-object` | `<name>-do` Durable Object namespace and `<name>-d1` D1 database |
| `kv` | `<name>-kv` KV namespace |
| `queue` | `<name>-queue` queue and `<name>-cron` cron trigger |

Nothing in the CLI reads this file back. It exists for the day you stop using HS-X: the names of every resource your project owns in your Cloudflare account, in a checked-in TypeScript module you can feed to your own tooling. That is why it is the one `.hs-x/` file the scaffolded `.gitignore` tracks (§06).

The identity exports are also where linked and unlinked deploys visibly diverge. A linked plan carries your HS-X account id and a control-plane counter deploy id (`deploy_email-guard_007`); an unlinked plan substitutes the machine-local owner (`local_865a8a52`) and a timestamped id (`deploy_email-guard_1781140081_ca9c`), which is why unlinked Worker names start with `hsx-local-…`.

## The cloudflare/ build directory and the state files

A Cloudflare push writes two files per worker under `.hs-x/cloudflare/`. The entrypoint, `<worker>.entry.ts`, wraps your worker module in the generated runtime bootstrap: install OAuth and managed-HubSpot token stores in both modes; checkpoint metrics, attestation, and the tenant data plane for flags and webhook dedup on linked deploys; the opt-in anonymous heartbeat on unlinked ones. The config, `<worker>.wrangler.toml`, names the Worker, points `main` at that entrypoint, sets `nodejs_compat`, and declares the bindings the deploy provisioned: `INSTALL_KV` always once an app id is known, plus `CHECKPOINT`, `FLAGS_KV`, `TENANT_DB`, and Workers Logs observability on linked deploys. Both are rebuilt on every push; the wrangler config doubles as the place `hs-x dev invoke --remote` learns the deployed script name.

The state files are smaller and longer-lived:

- `cloudflare.json` pins this project to a Cloudflare account and its `hsx-state` KV namespace, plus an owner id: `local_<hash>` until the project is claimed, an `acct_…` owner after `hs-x link` rewrites it.

  ```json
  {
    "cloudflareAccountId": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "stateKvNamespaceId": "4f9d2a7c1e8b4630a5d9c2f7b1e84a06",
    "stateKvNamespaceName": "hsx-state",
    "ownerId": "local_865a8a52"
  }
  ```

  It is written on the first deploy and read widely afterward: repeat deploys take the account id from it instead of requiring `CLOUDFLARE_ACCOUNT_ID` re-exported every time, `hs-x flags` loads it to reach the tenant state that holds the flag-authoring grant (no pointer means `No .hs-x/cloudflare.json found. Deploy this project first with hs-x deploy.`), unlinked `hs-x rollback` refuses to run without it, and `hs-x unlink`'s reset path is to delete it and redeploy.

- `project.json` binds this checkout to an HS-X account and project id (`{ "accountId": "acct_demo", "projectId": "email-guard" }`). `hs-x init` writes it when a session is active, `link` and `deploy` keep it fresh, and the project-scoped reads (`status`, `logs`, `routes`, `drift`) use it so those commands work inside a project without `--project-id` flags.

- `hubspot.json` records the app identity a successful HubSpot upload resolved: `{ "appId": 1234567, "projectName": "email-guard", "developerAccountId": "12345678" }`. The next deploy resolves its app id as flag, then `HSX_HUBSPOT_APP_ID`, then this file, so an app id the CLI already learned never has to be replayed.

- `last-invocation.json` is the failure-replay seed from `hs-x dev invoke`: the capability id, the exact payload, the response status, and a timestamp, which is what `hs-x dev invoke --last` re-runs.

- `dev-session.json` and `dev-server.log` exist only when an agent drives the dev loop through [the MCP server](/docs/mcp): the session record (pid, port, URL, log path) for the background `hs-x dev` process, and its captured output.

**Per-machine state is the one part of `.hs-x/` a deploy cannot fully rebuild**: deleting `cloudflare.json` mints a fresh `local_*` owner on the next deploy, and deleting `hubspot.json` makes the next upload re-resolve (or re-create) the HubSpot app rather than reuse it.

## What is committed, what is ignored, what ships

`hs-x init` scaffolds the policy directly into the project's `.gitignore`:

```sh
# HS-X generated (regenerated on dev/deploy).
# .hs-x/alchemy.run.ts is the leaveability artifact and IS tracked.
.hs-x/*
!.hs-x/alchemy.run.ts
```

Everything in the directory is ignored except the resource plan. The codegen output would churn every run if tracked, and the state files are per-machine by design; `project.json`'s account binding stays out of source control for the same reason HubSpot's own CLI keeps the active account out of project source.

One deliberate exception to the exception: after `hs-x link` claims a project, the CLI prints `Updated .hs-x/cloudflare.json. Commit the change so teammates inherit the link.` Under the scaffolded rules that file is still ignored, so a team that wants clones to inherit the claimed pointer adds a second exception line, `!.hs-x/cloudflare.json`, alongside the alchemy one.

**The `.hs-x/` directory never ships to HubSpot: the project archive a deploy uploads excludes it wholesale**, the same way it excludes `node_modules/`. Nothing here is part of your app's deployed source on either platform; the entrypoints under `cloudflare/` are bundled into the Worker by wrangler at push time, not uploaded as files.

## Which command writes what

| Command | Writes under `.hs-x/` |
| --- | --- |
| `hs-x init` | `project.json` (only when a session is active; best-effort), plus the `.gitignore` policy at the project root |
| `hs-x dev` | `manifest.json`, `refs.d.ts`, `refs.js` on startup |
| `hs-x dev invoke` | `last-invocation.json` after each dispatch |
| `hs-x deploy --plan` | `manifest.json`, `refs.d.ts`, `refs.js`; `alchemy.run.ts` once a project id is in play |
| `hs-x deploy` | All of the above, then `cloudflare/<worker>.entry.ts` and `<worker>.wrangler.toml` per worker, `cloudflare.json` on the first Cloudflare deploy, `project.json`, and `hubspot.json` after a successful HubSpot upload |
| `hs-x link` | Rewrites `cloudflare.json`'s `ownerId` from `local_*` to the claimed account; refreshes `project.json` |
| MCP dev-session tools | `dev-session.json`, `dev-server.log` |

`hs-x flags`, unlinked `hs-x rollback`, and the project-scoped reads (`status`, `logs`, `routes`, `drift`) stay read-only: when a file they need is missing, the error names the command that creates it rather than creating it on the spot. Deletion is yours alone, too. Nothing removes `.hs-x/` on your behalf; `hs-x unlink` names deleting `cloudflare.json` as the reset for a fresh `local_*` owner, but the delete is manual.

The deploy output confirms the codegen half explicitly: every run prints `Generated .hs-x/manifest.json and refs stubs.` before its final status line.

**Linked and unlinked projects produce the same set of files; the mode changes contents, not inventory.** The manifest and refs stubs carry no mode at all: codegen never sees whether the project is linked. Where the contents do diverge:

| Artifact | Linked | Unlinked |
| --- | --- | --- |
| `alchemy.run.ts` identity | HS-X account id, counter deploy id (`deploy_email-guard_007`) | `local_<hash>` owner, timestamped id (`deploy_email-guard_1781140081_ca9c`) |
| Worker names throughout | `hsx-<account>-<project>-<worker>` | `hsx-local-<hash>-<project>-<worker>` |
| `cloudflare/<worker>.entry.ts` | Attestation, checkpoint metrics, tenant data plane | Opt-in anonymous heartbeat only |
| `cloudflare/<worker>.wrangler.toml` | Adds `CHECKPOINT`, `FLAGS_KV`, `TENANT_DB`, and Workers Logs observability | Core bindings only (`INSTALL_KV` once an app id is known) |
| `cloudflare.json` `ownerId` | `acct_…` once claimed | `local_…` |

## Related reference

- [Deploy lifecycle](/docs/deploy-lifecycle) — what the deploy plan, the manifest hash, and the attestation recorded here gate on the control-plane side.
- [UI extensions](/docs/guides/ui-extensions) — the card-backend pattern the refs stubs exist for, with the full Email Guard example.
- [Getting started](/docs/guides/getting-started) — where `.hs-x/` first appears in a fresh project and what the scaffold lays down around it.
- [Runtime HTTP surface](/docs/runtime-http) — the `/_hsx/cards/<id>` dispatch route the refs point at, and its envelope.
- [CLI reference](/docs/cli) — the full flag surface for `init`, `dev`, `deploy`, `link`, and `flags`.
- [MCP server](/docs/mcp) — the dev-session tools that write the log and session record.

---

*Last updated: June 11, 2026. File shapes and regeneration behavior reflect the codegen and CLI shipped as `v0.2.5`. Refreshed when a new artifact lands or an existing shape changes.*

