# Installation

> Prerequisites (pnpm, pinned Node 22.14.0) and the three install paths: `pnpm run-local` for a single-command local stack, the hosted deploy wizard, and the starter repository. Documents what run-local builds, its source-hash stamp caching, and where local data is written.

- Repository: cloudflare/cloudflare-os
- GitHub: https://github.com/cloudflare/cloudflare-os
- Human docs: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd
- Complete Markdown: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd/llms-full.txt

## Source Files

- `scripts/run-local.mjs`
- `package.json`
- `README.md`
- `pnpm-workspace.yaml`
- `.gitlab-ci.yml`

---

---
title: "Installation"
description: "Prerequisites (pnpm, pinned Node 22.14.0) and the three install paths: `pnpm run-local` for a single-command local stack, the hosted deploy wizard, and the starter repository. Documents what run-local builds, its source-hash stamp caching, and where local data is written."
---

`pnpm run-local` is the single-command entry point for a local Cloudflare OS stack. It is a Node script (`scripts/run-local.mjs`) wired into the root `package.json` as `"run-local": "node scripts/run-local.mjs"`. It installs dependencies when needed, builds only the two outputs required to serve the app, then spawns `run-dev-server.js --serve-frontend-assets`, which serves the built frontend as static assets from the backend at `http://localhost:8787`. The whole stack runs on wrangler and workerd; the README states this is not meant for production use.

## Prerequisites

<ParamField body="pnpm" type="package manager" required>
The only supported package manager. The README's quick start is "install pnpm, then `pnpm run-local`". CI enables it via `corepack enable`.
</ParamField>

<ParamField body="Node.js" type="22.14.0">
The version CI pins. `.gitlab-ci.yml` sets `NODE_VERSION: "22.14.0"`, downloads that exact tarball from `nodejs.org`, and verifies it against `NODE_LINUX_X64_SHA256` before extracting.
</ParamField>

<ParamField body="git" type="optional">
`run-local` prefers `git ls-files` to enumerate source files for its hash. Without a git checkout (for example an extracted tarball) it falls back to a filesystem walk.
</ParamField>

<Note>
CI also runs `git submodule update --init` before `pnpm install --frozen-lockfile`, so a checkout with submodules should initialize them before installing.
</Note>

### Dependency install policy

`pnpm-workspace.yaml` defines the workspace as `packages/*` and constrains installs:

| Key | Value | Effect |
| --- | --- | --- |
| `minimumReleaseAge` | `1440` | Rejects dependency versions published in the last 24h, matching the CI supply-chain policy so a local install cannot commit a too-fresh lockfile. |
| `minimumReleaseAgeExclude` | `capnweb`, `capnweb-validate`, `workerd`, `@cloudflare/workerd-*` | Exempt from the release-age gate. |
| `overrides.workerd` | `>=1.20260623.1` | Minimum workerd for the local runtime. |
| `overrides` (pins) | `js-yaml@4.1.1 -> ^4.2.0`, `@codemirror/*`, `@types/node 26.1.0`, `@lezer/markdown 1.6.4` | Security patch (CVE-2026-53550) and mature-release pins. |
| `allowBuilds` | `@google/genai`, `core-js-pure`, `esbuild`, `protobufjs`, `sharp`, `workerd` | Only these packages may run install scripts. |

<Warning>
A too-fresh dependency will fail to install locally by design. `minimumReleaseAge: 1440` mirrors CI, which verifies every lockfile entry against the release-age policy.
</Warning>

## Path 1: `pnpm run-local`

<Steps>
<Step title="Install pnpm">
Follow https://pnpm.io/ . No other package manager is supported.
</Step>
<Step title="Run the single command">
```bash
pnpm run-local
```
Extra flags are forwarded verbatim to `run-dev-server.js`. For example, `pnpm run-local --use-workers-ai-binding` passes that flag through; `--serve-frontend-assets` is always added by the script.
</Step>
<Step title="Open the app">
Visit http://localhost:8787 . Expected console output before the server starts:

```text
Starting local server at http://localhost:8787 ...
```
</Step>
<Step title="Confirm it works">
Try a prompt from the README, for example "Make slides for my upcoming meeting with a customer." (uses the built-in slides blueprint) or "Make a collaborative whiteboard app." (creates a new app from scratch). Prompts that attach a GitHub repo or Google Doc require the corresponding integration to be configured.
</Step>
</Steps>

### What run-local builds

`run-local` deliberately builds only what is required to *run* the app — not a full-repo type-check and not the frontend `tsc` pass:

```bash
# scripts/run-local.mjs
pnpm --filter @gadgets/typed-storage build
pnpm --filter @gadgets/workshop-frontend exec vite build
```

| Output | Path | Why |
| --- | --- | --- |
| `@gadgets/typed-storage` dist | `packages/typed-storage/dist` | The backend imports it via its built `dist` output. |
| Frontend bundle | `packages/workshop-frontend/dist` | Served as static assets by the backend. |

Gatekeeper configurator files are not built here — `run-dev-server.js` generates them at startup.

```text
repo root
├── scripts/run-local.mjs        install -> build -> spawn server
├── run-dev-server.js            launched with --serve-frontend-assets
├── .run-local-stamp             source-hash stamp (written after a successful build)
├── node_modules/                presence gates the install step
└── packages/
    ├── typed-storage/dist/      built (backend imports dist)
    └── workshop-frontend/dist/  built (vite build) -> served on :8787
```

### Source-hash stamp caching

Repeat runs skip install and build entirely when nothing changed. The script hashes source file paths and contents with SHA-256 and compares the digest against `.run-local-stamp` in the repo root.

<AccordionGroup>
<Accordion title="How the hash is computed">
- File list comes from `git ls-files -z` plus `git ls-files -z --others --exclude-standard` (tracked files plus untracked-but-not-ignored files).
- If git fails, the script walks the filesystem, skipping `.git`, `node_modules`, `dist`, and `.wrangler`.
- Paths are normalized to `/` separators and sorted, so the digest is stable across platforms.
- For each file, the hash absorbs `path`, `\0`, contents, `\0`. A file listed but unreadable at hash time contributes `path\0<missing>\0`.
</Accordion>
<Accordion title="How the decision is made">
```js
const outputsPresent = existsSync(FRONTEND_DIST) && existsSync(TYPED_STORAGE_DIST);
const needsBuild = stamp !== sourceHash || !outputsPresent;
const needsInstall = needsBuild || !existsSync(NODE_MODULES);
```
Any source change — including a dependency change via `pnpm-lock.yaml`, since the lockfile is a tracked source file — flips the hash and triggers a rebuild. The stamp is written only after a successful build, so an interrupted build retries on the next run.
</Accordion>
</AccordionGroup>

Skip-path console output:

```text
Dependencies up to date; skipping install.
No source changes since last build; skipping build.
```

<Tip>
To force a full rebuild, delete `.run-local-stamp` (or remove `packages/workshop-frontend/dist` / `packages/typed-storage/dist`, since missing outputs also set `needsBuild`).
</Tip>

### Local data and generated paths

Everything `run-local` writes lives inside the repository checkout:

| Path | Written by | Contents |
| --- | --- | --- |
| `.run-local-stamp` | `run-local` | The hex SHA-256 source hash plus a trailing newline. |
| `node_modules/` | `pnpm install` | Workspace dependencies. |
| `packages/typed-storage/dist/` | `pnpm --filter @gadgets/typed-storage build` | Built module the backend imports. |
| `packages/workshop-frontend/dist/` | `vite build` | Frontend assets served on port 8787. |
| `.wrangler/` | local wrangler/workerd runtime | Excluded from the filesystem-walk hash alongside `.git`, `node_modules`, and `dist`. |

### Process lifecycle

The server runs as a child process spawned with `process.execPath` and `stdio: "inherit"`, so its logs appear in your terminal. On exit, `run-local` mirrors the child's outcome: if the child died from a signal it re-raises that signal on itself (`process.kill(process.pid, signal)`), otherwise it exits with the child's code (defaulting to `0`).

## Path 2: hosted deploy

Deploy to your own Cloudflare account from the hosted wizard at https://os.cloudflare.app/deploy .

<Info>
Gatekeepers are each implemented as a separate Worker. This repository ships several Gatekeepers you can deploy together with your own OS instance.
</Info>

## Path 3: starter repository

The intended adoption model is a fork, not a hosted multi-tenant service: "The idea is not that your company uses Cloudflare OS, but rather that you make it *Your Company* OS." The repository maps to OS concepts by directory:

| Normal OS | Cloudflare OS |
| --- | --- |
| kernel | `packages/workshop-backend` |
| device drivers | `packages/gatekeeper-*` |
| shell | `packages/workshop-frontend` |
| processes | gadgets |
| executables | blueprints |
| ACLs | shared permissions |

## Verify the install

Reproduce what CI enforces. `.gitlab-ci.yml` runs a blocking `lint` job and a `test` job that builds first:

<CodeGroup>
```bash lint (oxlint + recursive tsc --noEmit)
pnpm lint
```

```bash test (build first, then tests)
pnpm build
pnpm test
```
</CodeGroup>

Root script definitions:

| Script | Command |
| --- | --- |
| `build` | `pnpm run --recursive build` |
| `lint` | `pnpm run lint:check && pnpm run types:check` |
| `lint:check` | `oxlint` |
| `types:check` | `pnpm run --recursive --if-present types:check` |
| `test` | `node --test scripts/*.test.js && pnpm run --recursive --if-present test` |
| `clean` | `pnpm run --recursive clean` |
| `dev-server` | `node run-dev-server.js` |
| `dev-client` | `cd packages/workshop-frontend && pnpm run dev` |

<Warning>
Cloudflare OS is in heavy development. The repository is version 2, a complete rewrite of version 1; as of the August 2026 release it is described as capable but rough-edged early access.
</Warning>

## Troubleshooting

<AccordionGroup>
<Accordion title="run-local rebuilds on every invocation">
The stamp is only written after both builds succeed, and `needsBuild` is also true whenever `packages/workshop-frontend/dist` or `packages/typed-storage/dist` is missing. Check that both dist directories exist and that `.run-local-stamp` is writable in the repo root.
</Accordion>
<Accordion title="run-local reinstalls unexpectedly">
`needsInstall` is `needsBuild || !existsSync(node_modules)`. Any source change implies an install, so editing a tracked file (or the lockfile) will re-run `pnpm install`.
</Accordion>
<Accordion title="Install fails on a recently published dependency">
`minimumReleaseAge: 1440` rejects versions published within the last 24 hours, except the packages listed in `minimumReleaseAgeExclude`.
</Accordion>
<Accordion title="Node version mismatch">
CI pins `22.14.0`. Match that version locally to reproduce CI's `pnpm lint` / `pnpm build` / `pnpm test` results.
</Accordion>
<Accordion title="Prompts that need an integration fail">
README prompts that attach a GitHub repo or a Google Doc require the corresponding integration to be configured; the from-scratch and blueprint prompts do not.
</Accordion>
</AccordionGroup>

## Next

<CardGroup>
<Card title="Quickstart" href="/quickstart">Reach http://localhost:8787, confirm success, and run the first prompts.</Card>
<Card title="Local development" href="/local-development">The two-terminal `pnpm dev-server` / `pnpm dev-client` workflow and its flags.</Card>
<Card title="Overview" href="/overview">Package layout, the OS-analogy mapping, and runtime assumptions.</Card>
<Card title="Build, lint, and test" href="/build-lint-test">What CI enforces and the required ordering.</Card>
<Card title="Configure gatekeeper credentials" href="/configure-gatekeeper-credentials">Wire OAuth credentials into a connector for integration-dependent prompts.</Card>
<Card title="Troubleshooting" href="/troubleshooting">Known failure modes across the wider stack.</Card>
</CardGroup>
