Self-hosted environments are in public beta on Team and Enterprise plans; Availability and limitations covers the enablement path. This page is the CI test recipe; see the quickstart for setup and Deploy to production for the fleet recipes.
Install the capture hook on your test runner
The read-back works through a Claude Code Stop hook: when Claude finishes a turn, the hook receives the final assistant message aslast_assistant_message in its stdin JSON and appends it to $E2E_REPLY_DIR/<session_id>.txt. Install it the same way as the commit-nudge Stop hook, on the runner host’s ~/.claude/, which the runner seeds into every session.
Save the hook files
Save the two files below on the runner host:- The settings block: merge into
~/.claude/settings.jsonon the runner host - The script: save as
~/.claude/hooks/e2e-stop-hook-capture.shon the runner host and make it executable
Before you start the runner
Two things the hook depends on:- Install it before you start the runner. The runner snapshots
~/.claude/once at startup, so a hook added to a running runner takes effect only after a restart. - Export
E2E_REPLY_DIRto the runner process. The hook is a no-op when the variable is unset or the directory doesn’t exist, so set it wherever you start the runner, such as the systemd unit, pod spec, or CI step. The test script below requires it too.
E2E_REPLY_DIR exists, which is harmless on a throwaway CI runner but not something to carry into a production-environment runner image where the variable might be set by accident.
Run the test loop
The--environment and --ref dispatch flags require Claude Code v2.1.224 or later on the machine that runs the script, the same floor as the runner itself. With the hook in place and a runner started on this host, the test script:
- Creates a session on the test environment with
claude -p "<prompt>" --environment <environment-id> --output-format json, run from a git checkout so the CLI can auto-detect the repository from theoriginremote. The optional--ref <branch>bases the session’s checkout on a named ref instead of local HEAD. The command creates the session, prints one line of JSON containingsession_id, and exits without waiting for Claude’s reply. - Waits for the reply to appear in
$E2E_REPLY_DIR/<session_id>.txt, written by the Stop hook on the runner once the turn completes. - Sends a follow-up with
claude -p "<message>" --cloud <session_id> --output-format json(see Send a follow-up message to a running session), which posts a user event to the existing session and exits. - Waits for the follow-up’s reply the same way as step 2.
--environment dispatch behavior
In a non-interactive run, with -p or a piped prompt, Claude Code creates the session, prints the session ID and a link to it, and exits. From a terminal, claude --environment <id> "task" starts an attached interactive cloud session on the environment instead.
The flag takes precedence over the remote.defaultEnvironmentId setting. It doesn’t support --output-format stream-json, and can’t be combined with flags that resume, attach to, or preconfigure a session, such as --resume, --continue, --teleport, --session-id, or --init-only. --cloud is rejected with a session ID or URL, and in non-interactive runs when it carries a description. A bare --cloud is treated as absent. From a terminal, you can pass the task as the --cloud description instead of a positional prompt.
Example script
The script below runs the full loop against$CLAUDE_TEST_ENVIRONMENT_ID, your test environment’s ccpool_... ID, shown in the environment’s detail dialog on the admin page or returned by the create-environment call, and asserts on a sentinel phrase in each reply. Run it from a git checkout of the repository you want the session to work in, after starting a runner on this host with the capture hook installed and E2E_REPLY_DIR exported.