Cursor Cloud Agent notes
September 16, 2026 · View on GitHub
Kody runs on Cloudflare Workers: the origin app/MCP worker plus platform,
runtime, jobs, and status workers (Remix 3 UI + OAuth-protected MCP). See
local development for the local dev guide;
this document covers Cloud Agent VM gotchas only.
Node 26
The repo requires Node >=26 (engines in root package.json). Cloud Agent
VMs may ship Node 22 at /exec-daemon/node, which takes precedence over nvm
unless nvm’s Node 26 bin directory is prepended to PATH. Verify with
node --version before running scripts.
Playwright browsers
Playwright's Chromium (used by npm run test:e2e:run / validate) is
pre-installed in ~/.cache/ms-playwright and persists in the VM snapshot, so
normally nothing extra is needed. The non-obvious gotcha:
playwright install (and test:e2e:install / test:e2e:ensure) hangs on
this VM kernel — its Node-based zip extractor stalls on an io_uring write
partway through (around libwidevinecdm.so), and UV_USE_IO_URING=0 does not
stop it. The browser zip downloads fine; only the built-in extraction hangs.
If browsers are ever missing (e.g. a Playwright version bump changes the
revision), do not rely on playwright install. Instead download and extract
manually with native unzip:
- Get the revision + Chrome-for-Testing version from
node_modules/playwright-core/browsers.jsonand the CDN URL printed bynpx playwright install chromium(form:https://cdn.playwright.dev/builds/cft/<cft-version>/linux64/chrome-linux64.zipand.../chrome-headless-shell-linux64.zip). curl -fsSL -o /tmp/c.zip <chrome-linux64.zip>thenunzip -q /tmp/c.zip -d ~/.cache/ms-playwright/chromium-<rev>/andtouch ~/.cache/ms-playwright/chromium-<rev>/INSTALLATION_COMPLETE.- Repeat for the headless shell into
~/.cache/ms-playwright/chromium_headless_shell-<rev>/(Playwright launches headless via the separate headless-shell binary, so both are required). chmod +xthechromeandchrome-headless-shellbinaries.
Nx remote cache
Validate and test:push write Nx task artifacts. Those stay local unless the
self-hosted cache is configured. To populate GitHub Actions hits, set both on
the Cloud environment (not a one-off export). Use the write token
(NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN). Same-repo Actions validate uses
that token too; only fork pull_request jobs use the read token:
export NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://nx-cache.kody.codes
export NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN="$NX_CACHE_WRITE_TOKEN"
Use CI=1 on cached test commands (the repo scripts already do). Leave the
variables unset to run without remote cache. Those scripts run through
tools/run-nx.ts so a mid-run /v1/cache transport flake cannot fail
test:push or validate after the tasks already succeeded. See
packages/nx-cache/readme.md.
Git hooks
Cursor Cloud Agent VMs set core.hooksPath to a dispatcher under
~/.cursor/agent-hooks/ so Cursor can run secret-scan and co-author hooks.
npm run hooks:ensure (prepare runs it after husky) composes that
dispatcher with Husky: core.hooksPath stays on the dispatcher,
.cursor-original-hooks-path points at .husky/_, and pre-push /
pre-commit / commit-msg become dispatcher symlinks when those user scripts
exist. git push then runs npm run test:push (test:node + test:workers)
and can upload those Nx remote-cache artifacts before GitHub Actions starts.
Playwright E2E is not in the push hook: that suite is heavier than the unit
gate, and a failed e2e leg skips the unit gate when the push is retried with
--no-verify. Bundler artifacts live under src/node_modules/.kody-generated/
and wrangler-env clears that collector's additional-module watches and disables
esbuild's source-graph watcher in CLOUDFLARE_ENV=test
(WRANGLER_DISABLE_BUNDLE_WATCH) so wrangler dev does not loop on overlay
create events. Run npm run test:e2e:run or npm run validate for the
Playwright gate locally. wrangler-env.ts defaults X_LOCAL_EXPLORER=false on
dev because wrangler 4.127+ local explorer writes under .wrangler/tmp on
these VMs, retriggers esbuild, and leaves ProxyWorker in a pause/reload loop
after Ready. Opt in with X_LOCAL_EXPLORER=true. Wrangler 4.131+ keeps
Error inside ProxyWorker request-scoped (workers-sdk#15252), so a transient
ProxyWorker failure no longer exits the Playwright webServer.
Cloud Agent environment start should run npm run hooks:ensure so a snapshot
boot that skips npm ci still composes hooks after Cursor installs the
dispatcher. The command is a no-op on machines without ~/.cursor/agent-hooks.
Quick commands
| Task | Command |
|---|---|
| Install deps | npm install |
| Start or reuse dev | npm run dev:ensure (prints the resolved URL) |
| Migrate local D1 | npm run migrate:local |
| Seed test login | node tools/seed-test-data.ts --local (see seeding note below) |
| Full validate gate | npm run validate (CI runs the same checks as parallel jobs) |
| Manual PR preview | npm run preview:manual-test (see preview-manual-testing.md) |
| App verification | npm run control-kody -- doctor then login / request (--dump / --contains for HTML) / map --check / preview / health (see control-kody) |
Dev server
npm run dev:ensureis the agent entry point. It probes origin/healthon 3742–3751, printsApp running at http://localhost:<port>and exits 0 when a server is already up, waits for a kody/workerd leftover that accepts TCP but does not serve/healthbefore replacing it, then startsnpm run devand waits until/healthis actually ok before printing the resolved URL. Ifpackages/worker/.envis missing, it copies.env.examplefirst sonpm run dev(--env-file=packages/worker/.env) can start. If wrangler accepts TCP but Remix logsInvalid environment variables/Missing APP_DB binding(or the same forBUNDLE_ARTIFACTS_KV,STORAGE_RUNNER,PACKAGE_REALTIME_SESSION,MCP_CLIENT_HUB),dev:ensureexits immediately with that hint instead of waiting 180s. Those bindings come fromwrangler.jsoncvia the Vite Cloudflare plugin, not from.env; a snapshot that cannot provide local D1/KV/DO persist cannot serve/or/blog/*. If the latest wrangler line is Reloading and/healthstill misses the budget, the process is left running so a retry can reuse it. UI verification opens that real origin (for example/onboarding); do not substitute arenderToStringdump of one component.npm run devstarts the optional Cloudflare API mock, then Vite so origin SSR and the client hydrate in one workerd graph. Generated platform, runtime, jobs, and highlight configs join as Vite auxiliary workers (local D1/KV/DO persistence). Non-TTY sessions printApp running atonly after/healthresponds.- Default worker port is 3742 (
cli.ts); the CLI picks a free port when 3742 is taken and printsApp running at http://localhost:<port>. - Run long-lived interactive
npm run devin tmux so the session survives tool timeouts.dev:ensuredetaches the started process so the ensure command can exit. - Health check (no auth):
curl http://localhost:<port>/health→{"ok":true,"commitSha":...,"commit":...,"pullRequest":...,"deploy":...}. Locally the extra fields arenullunless a deploy var is set. Platform and runtime health paths (/__platform/health,/__runtime/health) 404 on the origin port.
Environment file
Copy packages/worker/.env.example to packages/worker/.env if missing.
dev:ensure does this copy itself. COOKIE_SECRET and SECRET_STORE_KEY are
required for local dev. The file does not create D1, KV, or Durable Object
bindings.
Seeding a test account
After npm run migrate:local, seed the local fixture logins per
seeding: kody@example.com / ilikecode (seeded with
the admin role) and jane@example.com / ilikecode (regular account). These
credentials are local test fixtures only. The seed script resolves the worker
Wrangler config automatically (same default as wrangler-env.ts), so
node tools/seed-test-data.ts --local works without extra flags.
Local limitations
- Vectorize bindings are not emulated locally; capability search uses the
offline ranker when
WRANGLER_IS_LOCAL_DEVis set (normal fornpm run dev). /mcpreturns 401 without OAuth; use browser login or MCP E2E tests for authenticated MCP checks.