@foldspace_npm/harness
Build the portable dist/index.js action artifact and verify it against a live
app.
This package is the part that is the same in both architectures. Everything else about how a builder runs differs between them; this does not.
The two architectures
| A — local (today) | B — remote (designed, unbuilt) | |
|---|---|---|
| Where the build runs | Claude Code on the human's computer | Claude Agent SDK in GCP |
| Whose browser | an isolated Chrome the harness launches | the human's own Chrome |
| How code reaches the page | harness CDP preparation | builder extension loads the cloud artifact |
| Prerequisites | Claude Code, a client repo, Node, an isolated Chrome profile | the extension installed |
| Session and cookies | in a profile the harness creates | the human's own, untouched |
| Code and history | a git repo on the human's computer | a workspace in the hosted environment |
"Export to repo" moves a build from B to A — and it is only a copy, not a
rewrite, while agent/ is identical in both. Keeping it identical is the
constraint this package exists to protect.
What is genuinely shared
agent/actions/*andagent/api/*— a handler isfetchplusrunTask, nothing more. Actions built in one track run unmodified in the other.agent/utils.ts— configures and re-exports@foldspace_npm/harness/runtimefoldspace build— esbuild →dist/index.js. In a code project, it first makesagent/runtime/match this harness's runtime (never rolling back a copy of a newer runtime; the version is insrc/runtime/VERSION) and names every file it wrotefoldspace deploy(Foldspace-internal) - uploadsdist/index.jstoagent/actions/<env>/<productId>/<agentApiName>- fixtures and tests
- the verb interface below
What differs — only the transport
| Verb | A (CDP) | B (extension) |
|---|---|---|
loadBundle | addScriptToEvaluateOnNewDocument | <script src> at the deployed CDN path |
evaluate | Runtime.evaluate | content-script message |
screenshot | Page.captureScreenshot | tabs.captureVisibleTab |
relaxCSP | Page.setBypassCSP | declarativeNetRequest header rules |
navigate | Page.navigate | tabs.update |
Detail and status: src/transports/README.md.
Why one package rather than two
Splitting harness-local and harness-remote forks build and deploy — the
exact failure this package was created to end. And the verb interface only stays
honest while both implementations sit behind it; separate them and "the same
action runs in both modes" quietly stops being true.
A hosted environment uses the same action source and build contract, but local
inject and attach remain CDP-only commands. The harness does not generate a
customer application extension.
Use
Discover the CLI
Human-readable help is grouped by workflow:
foldspace help
foldspace help attach
Coding agents can inspect the same versioned command and capability contract:
foldspace help --json
foldspace help attach --json
The installed harness binary is an alias for foldspace.
The JSON describes current options, prerequisites, side effects, risks, capabilities, and next steps. It does not claim that future hosted-environment capability enforcement is already implemented.
Create a project
Create a configured actions project from the template bundled with the installed harness version. Missing values are prompted when stdin is a terminal:
npx --yes @foldspace_npm/harness init
Non-interactive / CI form:
npx --yes @foldspace_npm/harness init foldspace-agent \
--product-id <product-id> \
--agent-key my-agent \
--app-domain app.example.com \
--name "My Agent"
When running directly from a harness checkout during development:
node bin/cli.mjs init ../foldspace-agent \
--product-id <product-id> \
--agent-key my-agent \
--app-domain app.example.com
The Agent Key is the value shown in Agent Studio, such as my-agent. It is
not the sidecar directory name. --agent-api-name remains a deprecated alias
for --agent-key. --domain remains a deprecated alias for --app-domain.
--name is optional and defaults to the target directory
name (foldspace-agent unless you pass a directory). The product ID must be
the bare ID, not the SDK key (the one that starts with EU-, in every
region). The app domain may be a hostname or an HTTP(S) URL without a port or
path.
--region us|eu (default us) is the Foldspace region the account lives in.
It is written to foldspace.dev.json as region, and picks the SDK the
project loads (sdkUrl): each region has its own build, pointing at that
region's servers. A project with no region is US. The values per region live
in src/region.mjs.
For safety, init sets up a new folder, or fills one that holds nothing but
what the system or an editor put there (.DS_Store, .localized, Icon\r,
._*, Thumbs.db, desktop.ini, .mcp.json, and the folders .git/,
.vscode/, .cursor/, .idea/, and .claude/ without settings.json). It
leaves those as they are and refuses anything else, including an existing
Foldspace project. It never fills the home directory or its Desktop, Documents
or Downloads, the disk's root, or the folder Claude Code Desktop makes for a
session started with no folder (under scratch-workspaces), or Codex's folder
for chats with no project (~/Documents/Codex) and each chat's folder in it,
even when they are empty. It never overwrites a file and does not initialize Git. On a TTY it can offer
npm install --ignore-scripts after scaffolding. After creation:
cd foldspace-agent
npm install --ignore-scripts # if you skipped the install prompt
# before the first build, CLAUDE.md step 5: a code project's agent/ replaces the scaffold's
npm run build
npm run inject # the human signs in in that window
npx foldspace observe wait-login
npx foldspace attach --daemon
The generated npm scripts intentionally remain the normal project interface;
foldspace init is the one-time project creation command.
Generated CLAUDE.md imports platform instructions from
@foldspace_npm/harness instead of copying them. Existing projects can switch
to that import with foldspace upgrade --refresh-instructions.
Keep the harness current
Tenant projects pin an exact @foldspace_npm/harness version. Publishing a
newer package does not move them: each session checks and bumps. Several
builders can work on one agent, so within one line (0.1.x to a newer 0.1.y,
update.autoUpgrade) the coding agent upgrades without asking and says so in
one line; across a line (0.1 to 0.2) it asks the human first.
npx --yes @foldspace_npm/harness@latest upgrade --check
foldspace upgrade --yes # within the line: no question; across a line: after the human agrees
--check (and a non-interactive upgrade with no --yes) never installs.
--yes writes the new exact version and runs npm install --ignore-scripts.
Rebuild afterward; that publishes nothing.
foldspace build and foldspace attach run the same check before they start
(attach --status and --stop do not). A project that is behind gets one line
on stderr and carries on. A project on a withdrawn version (below) refuses
to run and prints the reason. To build on it anyway, once the human has declined
the upgrade:
FOLDSPACE_SKIP_UPDATE_CHECK=1 npm run build
That turns the check off entirely; it does not fix the withdrawn version.
Keep up with the saved code
With a code project, several builders can save to one agent. foldspace sync
keeps a folder's agent/ up to date: it never reaches Foldspace itself - the
coding agent reads the saved code with the Foldspace tools and passes the
heads it read.
npx foldspace sync check --develop <sha> [--line <sha>] # what to pull, and where to write it
npx foldspace sync apply # merge it into agent/
npx foldspace sync saved --head <commit_sha> --paths <a,b> # after a save
--line is the head of foldspace/<Agent Key>, the line of work the code
project's instructions save to, once it exists. apply keeps a copy of
agent/ in .foldspace-dev/agent-kept/<time>/ and merges file by file
against what the folder last pulled (kept in .foldspace-dev/saved-code/): a
file only one side changed takes that side; a file both changed takes the
saved version and is listed under conflicts. Saving and publishing are
unchanged: they follow the code project's instructions.
Withdrawing a release (maintainers)
When a published version is broken badly enough that nobody should build on
it, withdraw it with npm deprecate. The registry is the list of withdrawn
versions, so projects already installed on it see the withdrawal without
installing anything new. Only owners of @foldspace_npm/harness on npm can
run it.
-
Publish the fix first. If the withdrawn version is also
latest, the message tells users to upgrade to a version that is itself withdrawn. -
Withdraw the broken version, with a message that says what is wrong and which version to move to. That message is exactly what users, and the coding agent working for them, see when
buildorattachrefuses to run:npm deprecate @foldspace_npm/[email protected] "attach loads the wrong bundle; upgrade to 0.1.15"A range works too:
npm deprecate "@foldspace_npm/harness@<0.1.15" "…". -
Check it took:
npm view @foldspace_npm/[email protected] deprecated
To undo a withdrawal, deprecate with an empty message:
npm deprecate @foldspace_npm/[email protected] ""
It is not instant for build and attach. They use the registry's answer
cached for 12 hours in .foldspace-dev/harness-update.json. foldspace help --json (which the manual reads first) and foldspace upgrade ask the registry
each time and refresh that cache, so a withdrawal reaches a project at its next
session start, and within 12 hours otherwise. It only reaches projects on a
harness version that has this check.
Lint agent code before it ships
foldspace build runs foldspace lint first. Errors skip bundling; warnings
print and still produce dist/index.js.
foldspace lint
foldspace lint --json
v1 flags:
- error
no-static-agent-prompt-execute, a card'scallbackor itsreadyStatehanding the agent instructions (directive, “tell the user”, …) - error
no-runtask-prompt—runTask({ data: { prompt } })and the same keys; a customer API field namedpromptis allowed - error
card-contract-awaitUserInput: trueon a view card (itsrenderComponentorrenderuses nocallback,cancelorreadyState): the chat panel ignores it, but in headless mode the agent waits for a callback that never comes - error
no-test-mode-in-bundle-armAllInstancesused anywhere inagent/(called, or handed on to be called later), orsetTestMode(...)with anything but a literalfalse. A code project's own copy of the runtime inagent/runtime/is not refused for thesetTestModeinside itsarmAllInstances - error
no-fixed-customer-address- on a product with a subdomain per customer (PER_CUSTOMER_SUBDOMAINinagent/constants.ts), one customer's address inAPI_BASEor anywhere inagent/; while that is undecided, an address whose first part looks like a customer's name (acme.- notapp.,eu1.,reports.) - error
token-stays-home- the user's sign-in token (getAuthToken, a session-named storage key,document.cookie, followed through variables and helper functions) returned fromexecuteor a card, handed to Foldspace, logged, stored again, or sent to a host that is not the product's backend - error
no-secrets- API keys, JWTs, bearer tokens, cookie values, secret-named strings,.harcaptures, storage dumps and.envfiles anywhere in the project (notnode_modules/,dist/,.foldspace-dev/,agent/runtime/) - warning
known-hosts-only-fetch,sendBeacon,WebSocket,EventSource,XMLHttpRequest,publicFetchorimport()to an address written out whose host is neither Foldspace's (*.foldspace.ai), the product's (foldspace.dev.jsonhosts,API_BASE, the app's own domain) nor written inagent/constants.ts - warning
card-contract- any otherawaitUserInputon a view card,awaitUserInputon an input card (not needed), a card that declarescallback,cancelorreadyStateand uses none of them (the SDK treats it as a view card), or arenderComponentthe SDK can't read (the object passed on or aliased,...rest,arguments): the agent waits until it callsreadyState,callbackorcancel, so a view card that calls none of them never gets a reply - warning
no-emoji— emoji in handler oragent/viewsstring literals - warning
registry-integrity— duplicate keys,{ execute }not registered, registry value missingexecute
Put action instructions in Agent Studio or with the Foldspace tools. foldspace lint cannot see that copy.
Runtime helpers
Tenant actions import from agent/utils.ts, which configures and re-exports
@foldspace_npm/harness/runtime. That code is bundled into dist/index.js and
runs in the customer's page — it is not a CLI command.
API_BASE and AUTH_SOURCE stamp empty. Fill them from a captured XHR, not a
guess. For auth that is not Bearer, follow
recipes/swap-the-login-method/README.md: for a custom header, copy its
agent/utils.ts over agent/utils.ts; for cookies with no token, its
variants/utils.cookies.ts.
Choose an attach mode
- Swap (default): the page already uses the configured product and agent.
- Bootstrap (
--bootstrap): the page has no Foldspace SDK. - Replace (
--replace): the page embeds a different product or agent, or has an SDK without the configured agent.
Use foldspace help attach for mode requirements, effects, and safety options.
Coding agents run foldspace attach --daemon so the invoking tool
returns after [lifecycle] inspect_registration:…. Foreground attach remains
the default for someone watching the terminal. attach --status and
attach --stop inspect and tear down a daemon. Look at the customer's app with
foldspace observe: it reads the test window, read-only, while attach runs.
Verify actions through the agent
foldspace attach does not invoke action handlers directly. It loads the local
artifact, verifies that the captured local registry matches the configured SDK
agent - including an empty registry - and then observes action calls while
you exercise the normal agent experience. Named actions are not required to
attach.
npx foldspace ask "<question>" --retry 2 types the question into the agent's
panel and reports the reply, the card text and a screenshot, and - while attach
runs - whether execute and render ran.
Action output shows the SDK's action calls and local execute/render phases:
[actions] sdk:action show_account_360 executed
[actions] local-handler:execute show_account_360 succeeded 412ms
[actions] local-handler:render show_account_360 succeeded 18ms
Only action names, parameter keys, statuses, and durations are recorded; handler
parameter values, results, and error messages are not. On detach, repeated page
failures and bounded action observations are summarized. The observer and
diagnostics are injected by the harness, so dist/index.js is not rewritten.
Check the published code
After the deploy tool in the Foldspace tools - not foldspace deploy, which
is for Foldspace staff only - publishes the code project to dev (or, with the
human's yes, to production), check it in the same test window. This needs a
site that loads this agent by itself: after attach --bootstrap or
--replace there is nothing to check yet.
npx foldspace attach --stop # while attach runs the page serves your local build
npx foldspace published dev # this page's agent now loads the published dev code
npx foldspace ask "<the question>" --retry 2
npx foldspace attach --daemon # back to your local build before you build anything else
published refuses while attach runs. It also refuses unless the page's own
Foldspace agent is this project's agent - the same product and Agent Key as
foldspace.dev.json, checked the way attach checks it. It changes only this
page's agent, through the SDK's setConfiguration({ enableDebugLogs: true, remoteActionsSettings: { enabled: true, environment: "DEV" | "PROD" } }); a
reload puts the page back on what the agent loads normally. When code loads,
the SDK keeps any handler the agent already has over a new one of the same
name, so once the published code has loaded, its handlers are set over the
page's for every action it defines; the page keeps its own handlers for the
rest. An agent pinned to one version of its code, or loading code from its
own address, is refused: the window would not show what was published last.
Without attach, ask still reports the reply, the card text and a
screenshot; handler.execute and handler.render read "not tracked here
(published code)".
Make a short video to share
npx foldspace record shows the agent's key use cases, from
video/scenes.json: an opening card meets the product's agent, then each
question is asked in the test window, typed in a new conversation, with a
caption once it is answered - the Foldspace features it shows as pills, the
question, and one large line on what the user gets, its payoff highlighted.
About 40 s, in Foldspace's colours with the official logo:
video/<product>-<date>.mp4 and a poster. It records with Chrome's own screencast and encodes in a hidden Google
Chrome - no ffmpeg, no screen-recording permission, nothing uploaded. The
video shows real data from the signed-in account; sharing it is the human's
call, and video/.gitignore keeps it out of git. The share-a-video skill,
written into each project's .claude/skills/, is the how-to. It runs for a
few minutes; stopped half-way, it puts the window back as it was.
Add the harness to an existing project
npm i -D @foldspace_npm/harness
{ "scripts": {
"dev": "foldspace build --watch",
"build": "foldspace build",
"inject": "foldspace inject",
"attach": "foldspace attach",
"attach:daemon": "foldspace attach --daemon"
} }
Every command resolves the consuming repo — process.cwd(), or
FOLDSPACE_PROJECT_DIR so a hosted builder can point it at a workspace it
controls. Config comes from the consumer's foldspace.dev.json.
foldspace deploy is Foldspace-internal: it writes to Foldspace's own storage,
which no customer can write to. A coding agent never runs or offers it. With a
code project, a build session publishes the agent's code with the Foldspace
tools instead: repo_write saves it, and the deploy tool publishes it - to
dev after saying so, to production only on the human's yes - as the manual's
"Done, and what you say at the end" says. Without a code project, the
Foldspace team does it with the human.
Why this package exists at all
scripts/ used to be copied into every client repo, and copies fork. By
2026-08-25 client-template held one fix and a client repo held four others,
and neither had all five:
| Fix | Why it matters |
|---|---|
| SDK load guard + retry | On an app with no document.body at document-start the append throws; the old guard tested for the stub, so nothing retried and the agent silently never loaded |
| Worker release on auto-attach | Auto-attach pauses every worker until the attaching client releases it — the harness froze the app's own workers for the whole session |
agentIds enumeration | agent({apiName}) returns the OVERLAY handle; arming it on an embedded agent leaves real conversations untagged |
--test-mode (off by default) | Test mode was armed unconditionally, hiding exactly what initial setup needs to see; now it is opt-in |
| Honest badge | It asserted TEST MODE whether or not test mode was on |
Two further changes came from packaging it: the bundle is injected rather
than fetched from a dev server — which removed express and concurrently, 95
packages, and is the precondition for driving a browser that cannot reach the
build machine — and inject now records the port that attach reads, since
they previously disagreed by construction.