Cloud environment: from nothing to a working claude --cloud session
How an adopter gets claude --cloud sessions on their own fork of himmel
starting with the harness working. The cloud lane
runs small, well-scoped tickets on an Anthropic-managed VM and opens a PR; a
local shepherd then reviews and merges it. What a session has, and why the
shepherd is mandatory, is in
cloud-brief-template.md. Platform
reference: cloud environments.
Why this is a paste, not a command
Checked against the docs and the CLI on 2026-10-10:
| Question | Answer | Source |
|---|---|---|
| Create or declare an environment from a repo file? | No. The dialog is the only path; the setup script is “entered in the Setup script field” of the environment settings dialog | cloud-environments |
| Create one from a command or API? | No. /remote-env “only sets the default: it doesn’t start a session, and it can’t add or edit environments”. claude --help has no environment subcommand | same page; claude --help (2.1.296) |
| Read an environment’s script or variables back from the CLI? | No. /remote-env lists names and IDs only | same page |
| Detect that the dialog differs from the repo? | No platform mechanism. A session sees CLAUDE_CODE_REMOTE=true and the variables you set | same page |
The Managed Agents environments API (POST /v1/environments,
ant apply environment.yaml) is a different product: API-key billed, its own
sessions, no env-var or setup-script field, and nothing connects it to
claude --cloud. It does not apply here.
So himmel keeps the dialog’s content in one checked-in source and checks the dialog against it:
| File | Role |
|---|---|
scripts/cloud/environment.env | The environment variables and the setup-script # rev: |
scripts/cloud/setup-env.sh | The setup script itself; the dialog only clones the repo and runs it |
scripts/cloud/check-env.sh | --print renders the dialog’s fields; with no argument a session checks itself |
Prerequisites
- A Claude subscription with Claude Code on the web (claude.ai/code), and the
claudeCLI on your machine. - Your fork of himmel on GitHub, with the Claude GitHub app installed on it:
at claude.ai/code connect GitHub and grant the app access to the fork, or run
/web-setupin a local terminal. The platform’s GitHub proxy then authenticatesgitandghinside the VM, so no token of yours enters it. - The fork’s
originset on the checkout you run the commands below from.
Create the environment
-
From your fork’s checkout, print the fields (it uses your
originas the clone URL, so a fork gets its own URL):Terminal window bash scripts/cloud/check-env.sh --print -
At claude.ai/code, select the cloud icon above the message box, then Cloud, then Add cloud environment. Paste the printed fields: Name
himmel, Network accessTrusted, Environment variables (the printed block), Setup script (the printed block).
Notes on the fields:
- Everyone who uses the environment can read the variables, so never put a secret there. The cloud has no Jira token on purpose: Jira goes through the claude.ai Atlassian connector, not the local jira CLI.
- Leave
GH_TOKENandGITHUB_TOKENunset so the GitHub proxy authenticates. - The two Bash timeouts must be set in this field: it is the only place they reach the Bash tool.
- Keep the setup script’s trailing
|| true: a non-zero setup script stops the session from starting, and a failed step should cost a tool, not the session. - The setup script installs shellcheck,
atand pre-commit, builds the Jira CLI dist, installs the obsidian-triage deps and the lean plugin profile (himmel-ops, lean-skills; a claude.ai plugin upload never loads in a cloud session), then graphify and qmd over the repo. Each build is bounded bytimeoutand a failure costs only that tool. Its last step stamps the script’s hash for the self-check below. - Network access
Trustedcovers GitHub, npm, PyPI and the Ubuntu archives, which is everything the setup and a session reach. Atlassian needs no entry: connector traffic goes through Anthropic, not the VM’s network. Pick Custom only for a host outside that list, and tick “Also include default list of common package managers”.
Make it the CLI default
claude --cloud does not use the claude.ai selector. In a local terminal run
/remote-env once and pick himmel; it saves remote.defaultEnvironmentId in
your user settings. Without it the CLI falls back to the Anthropic-hosted
Default environment, which has no setup script.
First session
claude --cloud "reply with the word ready, change nothing"The first session runs the setup script (about five minutes); watch it at the session URL the command prints. If it finishes inside about five minutes the platform snapshots the filesystem and later sessions start from the snapshot.
Verify it
In the session, run:
bash scripts/cloud/check-env.shExpect env <NAME> ok for each variable and setup-script ok, exit 0. A
mismatch is named on its own line and exits 1:
| Line | Meaning | Fix |
|---|---|---|
env <NAME> MISMATCH expected=... actual=... | The dialog’s variable differs from environment.env | Fix the dialog from check-env.sh --print, then start a new session (a resumed one keeps the values it last read) |
setup-script STALE | The cached snapshot ran an older setup-env.sh than this clone’s | Bump # rev: in the dialog’s setup script (and in environment.env) to rebuild the snapshot |
setup-script MISSING | No stamp: the setup never finished, or the dialog does not run setup-env.sh | Read /tmp/himmel-setup-logs/ for the failed step, or re-paste the setup script |
What the check cannot see: the bootstrap text in the dialog (the clone URL and flags) is not readable from inside the VM, so a changed URL shows only as a missing or stale stamp. The check is also not wired into a SessionStart hook yet: a cloud brief or the operator runs it by hand.
Then the tool probe:
echo CLAUDE_CODE_REMOTE=$CLAUDE_CODE_REMOTE BASH_DEFAULT_TIMEOUT_MS=$BASH_DEFAULT_TIMEOUT_MSshellcheck --version | sed -n 2pls ~/.claude/plugins# Inside the session's repo worktree, not /tmp/himmel-setup:bash scripts/cloud/setup-env.shgraphify query "cloud route classification" --graph graphify-out/graph.jsonbash scripts/lib/qmd-bounded.sh collection listbash scripts/lib/qmd-bounded.sh search "cloud environment" -c himmelExpect CLAUDE_CODE_REMOTE=true, BASH_DEFAULT_TIMEOUT_MS=600000, a shellcheck
version, a plugins directory, a Graph: ... nodes line, exactly one collection
(himmel) and hits from docs/. Then ask the session to list its skills: the
himmel-ops: and lean-skills: skills should be there.
What the cloud has, and what stays local
| The cloud has | How |
|---|---|
| Hooks | The repo’s .claude/settings.json; plugin hooks through the setup script’s plugin step (probed) |
| graphify | Pinned at scripts/lib/graphify-bin.sh, no backend extra, AST-only (graphify update .): it parses code locally and calls no model |
| qmd, repo only | The pinned fork and one collection, himmel, rebuilt on every setup run. BM25 only; a ticket that needs qmd query, vector search or another collection routes LOCAL-NATIVE |
| Jira | The claude.ai Atlassian MCP connector, enabled for the session |
| Stays local | Why |
|---|---|
| luna and any other vault | Private vault data never leaves the station (scripts/guardrails/egress-matrix.json) |
| Handover state | It lives in the state repo, which the cloud cannot reach; a cloud session reports through its PR |
| A Jira token | Not in the environment on purpose; use the Atlassian connector |
| Lane tickets (claudex, codex or another lane) | Cloud is Claude only; /cloud-route routes them LOCAL |
| The chief bridge and the hook-integrity off switch | Local launching-shell state; a ticket that edits scripts/hooks/ routes HOOK-BYPASS |
The cached graph and index live in /tmp/himmel-setup, a frozen clone. In the
session’s own worktree run bash scripts/cloud/setup-env.sh to rebuild both
against that checkout. Never run a semantic /graphify extraction in the cloud:
it would send content to a model backend.
Local-versus-cloud parity
bash scripts/cloud/parity-probe.sh is a read-only probe: one item<TAB>value
row per item under ## environment, settings, plugins, guards, gates,
tools, jira and egress. Run it locally and in a cloud session and diff the
two (--no-net skips the egress rows). Each PreToolUse guard row is a harmless
input the guard must refuse and reads DENY; ALLOW means that guard is off in
that environment. The probe changes nothing.
To run it in the cloud, start one session on the branch with the brief: run
bash scripts/cloud/setup-env.sh, then bash scripts/cloud/parity-probe.sh, and
post the full output as one top-level PR comment whose first line is
CLOUD-DONE <session URL>.
| Item | Local | Cloud (measured) | Gap and fix, or reason and ticket |
|---|---|---|---|
| Repo hooks (10 events, 40 commands) | wired, all scripts present | identical, all scripts present | none |
| Repo allow / deny rules | 156 / 37 | 156 / 37, same deny-sha | none |
User-scope deny rules and user CLAUDE.md | 36 deny, present | 0 deny, absent | Not closed here: the template would come from private user settings and rewriting live settings is VM-first. HIMMEL-5445 |
| Plugin set and plugin hooks | 12 installed, 81 hook files | himmel-ops, lean-skills; 2 hook files | By design: the lean profile installs at setup time. The PreToolUse guards below are the repo’s own hooks, not plugin hooks. HIMMEL-4206 |
| PreToolUse guards (9 deny probes) | all DENY | all DENY | none |
pre-commit binary | 4.6.2 | 4.6.2 | none |
| Git gate hooks (pre-commit, commit-msg, pre-push) | installed | closed here: a fresh clone had none; step git-hooks installs all three | The hook environments are fetched on first use, not at setup, to fit the ~5 min snapshot |
| Attestation and ticket-ID gate | refuses a ticketless message (rc 1) | same, rc 1 | none |
| bun | 1.4.4 | 1.4.2 | Image version, no action |
| node, jq, gh, git | 24.21.0, 1.8.2, 2.102.0, 2.56.0 | 22.22.0, 1.7, 2.89.0, 2.43.0 | Image versions lag the station; the probe and its checks ran on both. No action |
gitleaks, HIMMEL_REPO | present | closed here: step gitleaks of setup-env.sh installs gitleaks 8.30.1, sha256-pinned to the CI release; HIMMEL_REPO not set | HIMMEL_REPO: HIMMEL-4580 |
shellcheck, at | present | present | none |
| graphify | pinned 0.9.80 (the pin at probe time; scripts/lib/graphify-bin.sh holds the current pin) | image had 0.9.77, graph built AST-only. Closed here: setup skipped any present graphify; it now reinstalls one that differs from the pin, and the probe’s graphify-at-pin row names a drift | Build time against the budget: HIMMEL-4581 |
| qmd | 2.8.3 on PATH, 11 collections | installed by setup, no shim on PATH, so the probe read <absent>. Closed here: the probe resolves it through qmd-bounded.sh like setup does. One collection, himmel, BM25 only | By design: vaults and handover state stay local; a ticket needing a model-backed qmd query or vectors routes LOCAL-NATIVE |
| Jira access | local jira CLI | Atlassian MCP connector; the CLI dist is built but there is no token | By design: no token in the environment |
| Bash timeouts | unset locally | 600000 / 600000, set through the dialog | By design, see Notes above; check-env.sh reports a mismatch |
| Egress | unrestricted | pypi, npm, ubuntu archive 200; example.com 000 (outside the Trusted list, by design); a HEAD to github.com got 400 from the GitHub proxy | Closed here: the probe uses GET |
| luna, vaults, handover state, chief bridge, hook-integrity off switch | present | absent | By design, local only; two-way chief channel: HIMMEL-4277 |
The cloud column is one cloud run of the probe on this change’s branch:
setup-env.sh, parity-probe.sh and check-env.sh all exited 0.
How a cloud PR lands
/cloud-routeclassifies a ticket CLOUD-OK, writes the brief and prints the launch line; you run it (claude --cloud "<brief>").- The session works in a worktree, opens the PR and posts one top-level comment
whose first line is
CLOUD-DONE <session URL>, then the head SHA and test results. A block it cannot resolve is posted asCLOUD-BLOCKED <session URL>. - A local shepherd takes it from there:
bash scripts/handover/chief-kit/shepherd.sh <pr>runs the coverage lint, the impacted suites, CI and the ready check;/pr-checkand the CR gate run locally, and the chief merges onGO. The cloud session never merges and stops pushing once the shepherd comments. - The ticket stays
In Progressuntil the merge closes it.
Cache and refresh
- The snapshot is rebuilt when the setup script text or the allowed network
hosts change, and when it expires after about seven days. Plugins come from
the frozen
/tmp/himmel-setupclone, so a change onmainreaches new sessions only after a rebuild: bump# rev:inenvironment.env, re-print and re-paste the script. - A paused session that resumes does not re-run the setup script and keeps the variables it last read. Start a new session to pick up a change.