A how-to for provisioning Claude Code on the web (cloud sessions): what the pieces are, how to
set them up for any account or repository, and how this repository is set up. Details
deliberately live in the linked official pages, not here. Links into the three cloud pages
(web-quickstart, claude-code-on-the-web, cloud-environments) and the claims restated from
them were verified on 2026-08-13 against rung-1 raw-markdown fetches of those pages; links to
other pages were last verified 2026-07-30. Per the
upstream-drift convention, re-fetch a page before acting
on it.
- Claude Code on the web runs each
session in a fresh, isolated cloud VM with your repository cloned into it —
Anthropic-managed by default, or on an organization's
self-hosted environment when
routed there. The onboarding walkthrough (connect GitHub,
/web-setup, first task) lives on its own Get started page;claude-code-on-the-webis the full reference. - Every session runs inside a
cloud environment — the dialog with name,
network access, environment variables, and setup script. Environments are scoped to your
claude.ai account (or
shared org-wide by an admin),
not to a repository: one environment serves every repo and every surface that starts cloud
sessions (web,
claude --cloud, mobile, desktop, routines, and Claude Tag — whose channel sessions use org-shared environments only). - Two setup mechanisms exist, with an official division of labor: the environment's setup script provisions the VM itself (toolchains, CLI tools), while a repo-committed SessionStart hook handles project setup and runs in local and cloud sessions alike.
- What carries over from your setup
is the key reference: repo-committed
.claude/config reaches cloud sessions; user-level~/.claudeconfig never does.
Usually nothing to do — onboarding (the
browser flow, or
/web-setup from the CLI
if you already use gh) creates a
Default environment
whose Trusted network level already reaches the
default allowed domains
(common package registries, GitHub, SchemaStore). Configure an environment only when you need
more, and keep it repo-agnostic, since it serves all repos:
- Create or edit environments from the selector at claude.ai/code; pick a network access level if Trusted isn't right — this fleet's accounts all run All (see One environment or several?).
- Environment variables are readable by anyone who uses the environment and there is no secrets store — no credentials.
- A setup script is only for
tools missing from the
pre-installed inventory;
mind its requirements
— exit zero, finish within the roughly-five-minute cache-build budget, registries reachable at
the chosen access level —
and caching behavior.
The docs' worked example installs the
ghCLI, which pairs with the GitHub proxy for auth. - CLI users pick their environment with
/remote-env.
Everything repo-specific goes in source control, following the docs' pattern in Install dependencies with a SessionStart hook:
- Register a
SessionStarthook (matcherstartup|resume) in the repo's.claude/settings.json, pointing at a script in the repo via$CLAUDE_PROJECT_DIR. - In the script, exit immediately unless
CLAUDE_CODE_REMOTE=trueso local machines are never mutated, then install what the repo's own checks need. - Design rules that matter in practice: make every step idempotent (hooks run on every startup
and resume — see the
limitations list),
fail the session only for installs the session genuinely can't work without, and warn-and-
continue for the rest. Persist
PATHor other variables by appending to$CLAUDE_ENV_FILE. - Merge the hook to the default branch; from then on every cloud session on that repo picks it
up. In a cloud session you can also just ask Claude to create the hook — an Anthropic-provided
session-start-hookskill is preloaded there for exactly this.
Where a given piece of setup belongs, per the official split plus the cost model of environment caching:
-
Setup script (environment dialog; cached): heavy, repo-agnostic, static installs — SDKs (e.g. .NET, which the docs call out as setup-script material),
aptpackages, Docker image pulls. Runs as root; its cost is paid once per cache rebuild (script/network-config edit, or roughly-seven-day expiry), not per session. Total runtime must stay under the roughly-five-minute cache-build budget or sessions hang or fail at setup — parallelize independent installs and push oversized downloads into a SessionStart hook. -
SessionStart hook (repo-committed; every session start and resume): anything driven by the repo's own manifests or that must track branch state — dependency installs, pinned-tool provisioning. Runs locally and in the cloud, so guard cloud-only work with
CLAUDE_CODE_REMOTEand make every step idempotent; the cost is paid per session. -
Neither is for processes: the cache keeps files, not running services. Start databases or
docker composestacks per session (ask Claude, or start them from the hook). -
The setup script is the only pre-launch slot — plugins require it, and it caches the bootstrap's work: the setup script runs after the repository is cloned and before the session's Claude Code process starts, so a guarded line in the environment's setup script can run this repo's bootstrap and bake its results into the cached snapshot. That drops per-session hook time to the idempotent re-check (~3 s here) — and, more importantly, it is the only point where
claude plugin installcan land before the process reads its plugin registry, which is what makes plugins live in a session at all (see the same-session limit under Plugins in sessions on this repo):[ -f .claude/cloud-bootstrap.sh ] && CLAUDE_CODE_REMOTE=true bash .claude/cloud-bootstrap.sh || true
The guard keeps it a no-op for repositories without the script, so the environment stays generic, and this repo's ~40 s bootstrap fits comfortably inside the five-minute cache-build budget. (How the cache interacts with sessions across different repos isn't documented; the idempotent script makes either behavior safe.)
Start with one Default. Environments are account-scoped and repo-agnostic, so a single
environment serves every repository — this fleet runs its Default at All network access
(operator decision 2026-08-22; rationale in
CLOUD-FLEET-SETUP.md).
Add a second, named environment only when a class of work needs something incompatible or heavy
enough to isolate — a big SDK whose cache churn you want contained, or an account that handles
sensitive material and therefore has to run narrower than All, on a
custom domain allowlist.
A repo needing an uninstalled toolchain (the docs' example is the .NET SDK) means adding its
install to a setup script — extend Default, or create a dedicated environment and select it when
starting sessions on that repo. Reaching NuGet and dotnet.microsoft.com is not what settles .NET:
both are on the default allowlist, yet under Trusted the installer's redirect chain still came
back 403 (#2654 Blocker 1). That is why the toolchain question and the network-access question
are separate — and why the fleet answers the second with All.
The environment side stays generic (Default environment, All network access, no variables, the
gh setup-script one-liner and the guarded pre-launch bootstrap call from above). The repo side:
.claude/cloud-bootstrap.shis the bootstrap, with two callers: the account environments' setup scripts run it (withCLAUDE_CODE_REMOTE=true) after clone and before the session process launches — the only path that gets plugins loaded at turn one — and theSessionStarthook registered in.claude/settings.json(matcherstartup|resume) re-runs the same script per session start/resume as drift repair, since the environment cache can be ~7 days stale. Cloud VMs only; ~40 s on a fresh VM, ~3 s on re-runs. It provisions the tool inventoryci.ymlpins, reading in-repo manifests wherever one exists:
| Tool | Pin source | Required? |
|---|---|---|
| Node | .node-version (via the VM's nvm) |
required — CI pins a major the VM image doesn't ship |
| claude CLI + Biome + markdownlint-cli2 | root package-lock.json (npm ci) |
required — markdownlint stays repo-local so the markdown-format hook's node_modules/.bin probe (and a ~/.local/bin symlink the bootstrap adds for PATH-based resolution) can see it; npm -g into the nvm prefix is invisible to hooks (#2739 / #2748) |
| ruff, pytest, pyyaml | .github/requirements-ci.txt (hash-locked) |
required — --require-hashes fails closed |
| shellcheck, actionlint, typos, editorconfig-checker, gitleaks | pinned in the bootstrap (GitHub release binaries → ~/.local/bin) |
best effort — warns and continues |
| check-jsonschema | pinned in the bootstrap (uv tool / pip --user) |
best effort |
full git history + origin/main |
git fetch |
best effort — the base-ref diff gates need it |
| the enabled plugin catalog | enabledPlugins in .claude/settings.json |
best effort — a plugin that fails to install costs its skills, not the session |
The bootstrap's startup report_tool resolves each binary under a hook-safe PATH
(the process PATH with the nvm prefix stripped) and prints the resolved path, so an
npm -g install that only the SessionStart shell can see cannot print false-green
again. CLAUDE_ENV_FILE PATH repairs still reach subsequent Bash tool calls only —
hook processes inherit Claude Code's own environ, which includes ~/.local/bin but
not the nvm global prefix.
Best-effort rather than required, deliberately: the plugin contract suites SKIP visibly when an optional tool is absent and CI remains the enforcing gate, while a required install failure would block the session from starting. GitHub release-asset downloads are additionally best-effort because the GitHub proxy documents that release assets from repositories not attached to the session can return 403.
Not installed at session start (install on demand when working in those areas): the plugin npm
packages — plugins/miro and .github/standards/runner-policy are each an npm ci in their
own directory, while the video-digest, course-digest, and ai-briefing suites install through
their skills' entry scripts (plugins/knowledge/skills/video-digest/scripts/run-tests.sh install, plugins/knowledge/skills/course-digest/scripts/run-tests.sh install,
plugins/ai-briefing/skills/generate/scripts/run-tests.sh install); the heavy ones pull
Playwright. gh, pwsh, and lychee are likewise on-demand.
Being the marketplace doesn't make this repo's plugins active in a session — plugins load only
when a marketplace is declared, enabled, and installed. .claude/settings.json declares and
enables; the cloud bootstrap installs (see
Discover and install plugins and
extraKnownMarketplaces / enabledPlugins):
extraKnownMarketplacesdeclares this repo as its own marketplace via adirectorysource with a relative path, so a session exercises the plugin code on the current branch rather than publishedmain. Local collaborators are prompted once they trust the folder.- A declared marketplace is gated on workspace trust, and cloud sessions arrive untrusted.
What runs before you trust a folder
groups
extraKnownMarketplacesentries with the content that needs this exact folder trusted, while hooks and theenvblock are used whether or not it is. Observed on 2026-08-15: a cloud session on this repo hadprojects["<repo-root>"].hasTrustDialogAcceptedset tofalsein~/.claude.json, an empty~/.claude/plugins/installed_plugins.json, no plugin skill loaded and every/plugincommand unknown — while the same settings file'senvblock had applied. That is exactly the split the table predicts, and it is why what carries over promising plugins "installed at session start from the marketplace you declared" did not hold here. Hooks run untrusted, so a hook can repair the on-disk state — but not the running session; see the next bullet. - A SessionStart install is never visible to the session that ran it. Observed 2026-08-15 in
a cloud session on this repo: the hook completed
65 enabled, 65 newly installed, 0 failed,~/.claude/plugins/installed_plugins.jsonand user-scopesettings.jsonwere fully populated with the whole catalog — yet the same session's plugin registry stayed empty: its first message, a plugin slash command, returned "Unknown command", and a mid-session probe of the skill registry resolved no plugin skill. The command/skill registry is built when the Claude Code process starts, before SessionStart hook effects land, and is not re-read afterwards; the hooks reference documents no same-session pickup, and neither/pluginnor--plugin-direxists in cloud sessions to force one. On an ephemeral VM this is a chicken-and-egg: every fresh session re-installs after its registry is already built, so the hook alone can never produce a session with plugins loaded. What the hook still buys is correct on-disk state for any process start that happens after it — a resume (confirmed 2026-08-15: stopping and resuming the same session restarted the process, which re-read the registry and loaded the full catalog, plugin skills resolving from the first post-resume turn), and (the fix for turn one) the environment setup script running this same bootstrap at cache-build time, before any session process launches: the guarded one-liner in the setup-script lever above, implemented fleet-wide by the standardscloud-environmentcomponent that CLOUD-FLEET-SETUP.md's step-1 stub fetches. Whether the cached snapshot's~/.claudeactually reaches sessions is undocumented — after adding the line, rebuild the cache (edit saves the script) and verify with a fresh session whose first message is a plugin slash command. - Harness residual — first-turn slash of just-installed plugins (#2733). The "Unknown
command" outcome above is not remediable inside any plugin in this repository: the
command registry is a Claude Code harness property (built at process start, not re-read).
Track occurrences via
/claude-ops:known-issuesand, when reproducible on a fresh cloud session after a confirmed pre-launch bootstrap, report upstream (anthropics/claude-code) with the bootstrap log plus the first-turn transcript. In-session workarounds when a first-turn slash returnsUnknown command:(a) resume the session so the process restarts and reloads the registry, or retry the slash on a later turn after a resume; (b) direct-file fallback — readplugins/<plugin>/skills/<skill>/SKILL.mdfrom the repo working tree and follow it manually (note: this bypasses skill-load string substitutions such as${CLAUDE_EFFORT}). Prefer fixing the environment so the setup-script path pre-installs before process start; do not invent plugin-side registry hacks. - Being a
directorysource may compound it — that source is documented for development only and the carry-over note qualifies install-at-session-start with "requires network access to reach the marketplace source". The two candidates were not separated, because the trust gate alone accounts for the symptom and the bootstrap makes both moot. - The bootstrap therefore registers the checkout by absolute path and installs the enabled set
explicitly — for the benefit of the next process start, per the timing bullet above. It
never calls
claude plugin marketplace remove, which deletes the marketplace's entry from.claude/settings.jsonand would have the script mutate tracked config. - On resume it also repairs same-version commit drift: because a
directory-source cache is keyed by the semver in
plugin.jsonrather than the commit, a presence check alone would keep serving whichever commit installed first. The script compares thegitCommitSharecorded at install time againstHEADand forces the documented uninstall/install/enable cycle for the plugins whose own directory changed between the two, so the usual resume stays cheap. Uncommitted edits are out of scope by design — useclaude --plugin-dir ./plugins/<name>, which takes session precedence over the cached install. - Consumer repos should declare the marketplace with a
githubsource —{"source": "github", "repo": "melodic-software/claude-code-plugins"}— since the relativedirectorysource is specific to this repo, whose reason to exist is validating in-flight plugin changes. Declaring it is necessary but, per the trust gate above, not sufficient in a cloud session; verify in a fresh session and add the same bootstrap-plus-hook setup if the catalog does not load. enabledPluginsturns on the whole catalog, so this repo dogfoods everything it publishes and a regression in any plugin surfaces here first. The trade is context: every enabled plugin adds per-turn cost, so a consumer repo should enable only the plugins it needs rather than copying this set wholesale. The cloud bootstrap provisions every tool the format/lint-on-edit hooks (markdown-format,bash-format,biome-format,typos-format,actionlint,eol-normalizer) shell out to.- The
plugin-catalog-enablement-gateCI lane holds that whole-catalog claim to the file, in both directions: every.claude-plugin/marketplace.jsonentry must carry anenabledPluginskey, and every key for this marketplace must name a catalogued plugin. It also checks thatcloud-bootstrap.sh's hardcodedmarketplace_namestill names the marketplace the settings file declares — the bootstrap selects what it installs withendswith("@" + $n), so a rename that updated the settings and the catalog but not that constant would leave its install set empty while the parity lane stayed green over it. It exists because the claim was prose for three plugin releases that shipped catalogued but never enabled — a silent failure, since the bootstrap computes its install set from the same map and a session simply comes up without those skills.claude-config'scheck-plugin-drift.shcannot cover it: that detector resolves each marketplace throughsource.repoand records SKIP for one declaring none, which is precisely this repo's relativedirectorysource. - Entries are sorted alphabetically, one per line, so a single plugin can be flipped to
falsewithout disturbing the rest — a state the gate accepts, since an explicitfalseis a recorded decision where an absent key is drift. Two entries carry requireduserConfigcredentials that are unset here —miro(miro_api_token) anddometrain(dometrain_api_key) — so their bundled MCP servers exit at startup until configured; set them with/plugin configure, or flip those two tofalseif a session shouldn't try.
Both exist in cloud sessions and don't conflict — they serve different callers:
- The built-in GitHub MCP tools are how the agent itself reads issues, PRs, and CI; they authenticate through the GitHub proxy with no setup.
- The
ghCLI is what this repo's plugin scripts and hooks shell out to (severalsource-control,guardrails, andwork-itemssuites SKIP without it). It isn't pre-installed; the environment setup script installs it, and in cloud sessions it authenticates via the proxy automatically — no token needed. Locally, contributors authenticateghthemselves as usual.
- Some bootstrap pin sources are materialized from
melodic-software/standards(see the sync manifest) — of the files the bootstrap reads,.node-versionis in the synced set (verified against thechore: sync standards componentshistory on 2026-07-30), so its Node pin updates arrive via sync..claude/settings.jsonand the bootstrap script itself are repo-owned. .github/requirements-ci.txtis hash-locked. The lockfile carries ABI-specific hashes for both the CI interpreter (cp314 / 3.14) and the cloud VM system Python (cp311 / 3.11; #2657), so the bootstrap always installs with--require-hashesand a digest mismatch stays fatal — no interpreter-mismatch skip, no unpinned fallback. Installs usepiprather thanuv, whose PyPI fetches time out against the VM's egress proxy.- The bootstrap's own version pins exist only because those tools have no in-repo manifest; the
cloud proxy blocks the GitHub API and
releases/latestredirects, so the script can't self-resolve "latest". Each GitHub-release asset also carries a pinned SHA-256 the script verifies before installing (mismatch refuses the install and warns). Bump pin and hash together when the corresponding configs bump.