
The previous article covered how to embed Code-Server (OpenVSCode Server) into the Home Assistant sidebar as an Ingress panel — giving you a full browser-based VS Code editor accessible from within your HA interface. If you haven’t set that up yet, start there first.
This article goes one step further: adding Claude Code — Anthropic’s AI coding assistant — directly into that Code-Server environment. The result is an AI-powered editor embedded inside Home Assistant, reachable through your existing HA reverse proxy with zero additional ports exposed.
This is not a simple npm install. Getting Claude Code working inside gitpod/openvscode-server behind an Ingress proxy requires solving several compatibility problems that don’t exist in a standard VS Code Desktop install. This guide walks through all of them.
Prerequisites
- Home Assistant Docker setup with the Code-Server Ingress panel already working (follow the previous guide)
gitpod/openvscode-servercontainer running on your Docker host- An Anthropic account — you’ll need an API key (free tier available)
- Basic familiarity with editing
docker-compose.yml
Architecture Overview
Nothing changes in the network topology — Claude Code runs entirely inside the existing code-server container. The additions are the API key, the Claude Code extension, and a startup script that wires everything together correctly.
Claude Code communicates outbound to Anthropic’s API over standard HTTPS. It does not require any inbound port, firewall rule, or nginx change.
Step 1 — Get Your Anthropic API Key
- Go to console.anthropic.com and sign in or create an account.
- Navigate to API Keys → Create Key.
- Give it a descriptive name (e.g.,
home-assistant-code-server) and copy the key — you won’t see it again.
Anthropic offers a free usage tier that is more than enough to get started. Paid usage is billed per token above the free limit.
Why the key MUST be a Docker environment variable:
Claude Code has two authentication methods: a browser OAuth login flow (claude login), and the ANTHROPIC_API_KEY environment variable. On a headless Docker server there is no browser — the OAuth flow is impossible. The environment variable is the only viable authentication path in this setup.
But the reason it must live in the Docker environment: block specifically — not in a shell config file, not in ~/.claude, not anywhere else — comes down to how the process tree works inside the container.
When openvscode-server starts, it spawns a separate extension host process (extensionHostProcess.js) as a child. That process in turn spawns isolated worker processes for each installed extension. Claude Code runs in one of those workers. All of these child processes inherit their environment from their parent, which inherits from the container. The chain looks like this:
Environment variables set inside the container after startup — for example in .bashrc or .profile — only apply to interactive login shells. They are never visible to the extension host subprocess chain, because that chain is started directly by the server process, not by a login shell. The Docker environment: block is the only mechanism that injects variables into the process environment at the container level, making them available to every child process automatically.
The same env var is also read by the bundled claude CLI binary when you run it in the integrated terminal — so one declaration covers both the UI extension and the terminal tool.
In the compose file the key is declared directly:
environment:
- TZ=YOUR_TIME_ZONE
- ANTHROPIC_API_KEY=sk-ant-YOUR_KEY_HERE⚠️ Git security: If your docker-compose.yml is in a Git repository (it should be), move the value into a .env file at the same path and add it to .gitignore. Docker Compose reads .env automatically, so you can reference the variable without its value in the compose file and the behaviour is identical:
# /opt/HA/.env — add to .gitignore
ANTHROPIC_API_KEY=sk-ant-YOUR_KEY_HEREenvironment:
- TZ=YOUR_TIME_ZONE
- ANTHROPIC_API_KEY # value loaded from .envThe authentication mechanism is the same either way. The .env approach only prevents the key from being committed to version control — it does not change how or where Claude Code reads it.
Step 2 — Prepare Host Directories
All the following steps require root permissions, so get them in your console:
sudo -sCreate the required host directories before applying the new configuration:
mkdir -p /opt/HA/code-server/data
mkdir -p /opt/HA/code-server/extensions
mkdir -p /opt/HA/code-server/claude-configThis step must come first because of a critical ordering constraint: the new config mounts /opt/HA/code-server/extensions as a Docker volume into the container at /home/workspace/.openvscode-server/extensions. The Claude Code extension must be installed after this volume exists and is mounted — so that what you install lands on persistent host storage, not inside the container’s ephemeral filesystem.
⚠️ Why you cannot use the previous article’s config to pre-install the extension:
The previous article’s docker-compose.yml had no separate extensions volume mount. Any extension installed through the UI with that config went into the container’s internal filesystem at /home/workspace/.openvscode-server/extensions — a path that was never mounted to anything on the host. When the new config is applied with the extensions volume, that mount starts completely empty regardless of what was installed before. The extension installation must happen after the new config is applied.
Step 3 — Docker Compose Configuration
Replace your existing code-server service definition with the full configuration below. Every section is explained in the ⚠️ Explanation block that follows.
code-server:
image: gitpod/openvscode-server:latest
container_name: code-server
hostname: code-server
restart: unless-stopped
user: "root"
dns:
- YOU_DNS_SERVER
entrypoint: ["/bin/sh", "-c"]
command:
- |
# 1. Apply the Navigator Patch (Fixed to prevent infinite loop)
sed -i 's|get:()=>{[^}]*navigator is now a global in nodejs[^}]*}|get:()=>({userAgent:"code-server"})|g' /home/.openvscode-server/out/vs/workbench/api/node/extensionHostProcess.js
# 2. Fix Permissions
chown -R openvscode-server:openvscode-server /home/workspace/.openvscode-server
chown -R openvscode-server:openvscode-server /opt/HA
# 3. Setup Claude Config Link
mkdir -p /opt/HA/code-server/claude-config
chown -R openvscode-server:openvscode-server /opt/HA/code-server/claude-config
ln -sf /opt/HA/code-server/claude-config /home/workspace/.claude
# 4. Find the Claude extension folder dynamically
CLAUDE_PATH=$$(find /home/workspace/.openvscode-server/extensions/ -name "anthropic.claude-code-*" -type d | head -n 1)
if [ -n "$$CLAUDE_PATH" ]; then
echo "Found Claude at $$CLAUDE_PATH"
ln -sf "$$CLAUDE_PATH/resources/native-binary/claude" /usr/local/bin/claude
else
echo "Claude extension folder not found!"
fi
# 5. Start the server
exec runuser -u openvscode-server -- /home/.openvscode-server/bin/openvscode-server \
--host 0.0.0.0 \
--port 8080 \
--server-base-path /api/ingress/code_server \
--without-connection-token \
--default-folder /opt/HA \
--extensions-dir /home/workspace/.openvscode-server/extensions \
--enable-proposed-api Anthropic.claude-code
ports:
- "8080:8080"
volumes:
- /opt/HA/code-server/data:/home/workspace/.openvscode-server/data
- /opt/HA/code-server/extensions:/home/workspace/.openvscode-server/extensions
- /opt/HA:/opt/HA
environment:
- YOUR_TIME_ZONE
- ANTHROPIC_API_KEY=sk-ant-YOUR_KEY_HERE
labels:
- updates2mqtt.name="Code Server Update"
- updates2mqtt.compose_path=/opt/HA/docker-compose.yml
- updates2mqtt.picture=https://registry.coder.com/module/code.svg
networks:
home_assistant:
aliases:
- code-server
ipv4_address: 172.20.0.4⚠️ Explanation — Every Design Decision
This configuration is meaningfully more complex than a standard code-server setup. Here is exactly what each part does and why it cannot be simplified.
user: "root" — Root Startup for Privileged Initialization
The container starts as root, which differs from the previous code-server config that used 1000:1000. Root is required here because the startup script must run sed to patch a file owned by root inside the image, run chown on mounted volumes, and create a symlink under /usr/local/bin. None of these are possible as an unprivileged user. After all setup steps complete, the server process itself is launched as the unprivileged openvscode-server user via runuser — root is only held during initialization.
entrypoint + command — Full Startup Override
The gitpod/openvscode-server image has its own entrypoint that starts the server immediately. Overriding entrypoint with /bin/sh -c replaces that entirely with a custom shell. The command block then passes the multi-step setup script as a single string (the | YAML block scalar). This pattern gives complete control over what happens before the server starts — something that cannot be achieved with the default command override alone.
Startup Script Step 1 — The Navigator Patch
This is the least obvious line in the entire config and the most critical one for Claude Code specifically:
sed -i 's|get:()=>{[^}]*navigator is now a global in nodejs[^}]*}|get:()=>({userAgent:"code-server"})|g' \
/home/.openvscode-server/out/vs/workbench/api/node/extensionHostProcess.jsThe root cause. Node.js 21 added navigator as a built-in global — matching the browser Web API. The VS Code extension host (extensionHostProcess.js) was written for an earlier world where navigator did not exist in Node.js. It defines its own internal navigator getter meant to satisfy browser-targeting extensions. When Node.js now also provides navigator as a global, the two definitions conflict. Extensions that check navigator.userAgent — which Claude Code does for environment detection — can receive wrong values, throw errors, or in some versions enter infinite call loops depending on how the JavaScript runtime resolves the conflict.
What the patch does. The sed command locates the existing complex getter (identified by the comment string "navigator is now a global in nodejs" embedded in its source) and replaces the entire getter body with a minimal implementation: get:()=>({userAgent:"code-server"}). This returns a plain object with a userAgent string — exactly what Claude Code’s environment checks require — without touching Node.js’s own navigator global.
Why “Fixed to prevent infinite loop”. An earlier version of this patch produced a getter that referenced itself, causing a stack overflow. The current literal-object version avoids any self-reference.
Why it runs on every container start. Pulling gitpod/openvscode-server:latest replaces the image, including extensionHostProcess.js, with an unpatched version. Applying the patch in the startup script re-applies it automatically on every restart, surviving image updates with no manual intervention.
Startup Script Step 2 — Permission Fix
chown -R openvscode-server:openvscode-server /home/workspace/.openvscode-server
chown -R openvscode-server:openvscode-server /opt/HA Docker creates volume mount points owned by root by default. Even if host directories were pre-created, ownership can revert in certain Docker and filesystem configurations. Since the server process runs as openvscode-server, it needs write access to its data directory and the HA config mount. Running chown once at startup as root guarantees correct ownership on every start, regardless of how the host prepared those directories.
Startup Script Step 3 — Claude Config Persistence
mkdir -p /opt/HA/code-server/claude-config
chown -R openvscode-server:openvscode-server /opt/HA/code-server/claude-config
ln -sf /opt/HA/code-server/claude-config /home/workspace/.claudeClaude Code stores all of its persistent state in ~/.claude — inside this container that maps to /home/workspace/.claude. Without this symlink to a volume-backed location, the entire directory is lost on every container recreation. Understanding exactly what lives there explains why losing it matters so much.
The ~/.claude directory structure:
Each piece has real consequences if lost:
settings.json holds your configured preferences — whether Claude asks for confirmation before applying file edits, auto-approval settings, UI preferences. These accumulate as you tune the tool to your workflow. Losing them resets every preference to its default.
projects/<hash>/CLAUDE.md is the most important file in the directory. Claude Code automatically reads CLAUDE.md files from two locations when you open a project: from the project root (/opt/HA/CLAUDE.md), and from this per-project entry in ~/.claude. This file acts as a persistent system prompt for your project — a place to describe your setup so Claude starts every session already knowing your context. For a Home Assistant project, a useful CLAUDE.md might look like:
# Home Assistant Project Context
- Config files: /opt/HA/homeassistant/
- Automations: /opt/HA/homeassistant/automations/
- Docker Compose: /opt/HA/docker-compose.yml
- MQTT broker: Mosquitto on 172.20.0.2
- Zigbee: Zigbee2MQTT with Sonoff dongle
- Do not restart HA automatically — ask firstOnce you create this file (or let Claude generate it with claude init), every future conversation in this project starts with that context already loaded. Claude knows your directory structure, your broker address, your naming conventions. Without ~/.claude persistence, this file disappears on every container recreation and you rebuild it from scratch each time.
projects/<hash>/conversations/ stores conversation history so Claude can reference what was discussed in previous sessions. This is what allows Claude to say “last time we worked on this, we decided to…” — it has actual memory of past interactions, not just the current conversation window.
todos/<hash>.json persists task lists across sessions. If you start a multi-step refactoring job — “rename all these entities, then update the dashboard, then check the automations” — Claude tracks what is done and what is pending between separate conversations.
Why symlink rather than a fourth volume mount:
ln -sf /opt/HA/code-server/claude-config /home/workspace/.claudeThe persistence outcome of a symlink versus adding - /opt/HA/code-server/claude-config:/home/workspace/.claude to the volumes: block is identical. The symlink approach is used here because it keeps the volume list clean and, more importantly, keeps the entire persistent state of the setup — HA config, code-server data, extensions, and now Claude’s memory — under a single /opt/HA directory tree. One rsync or backup job covers everything.
Startup Script Step 4 — Dynamic Claude Binary Discovery
CLAUDE_PATH=$$(find /home/workspace/.openvscode-server/extensions/ -name "anthropic.claude-code-*" -type d | head -n 1)
if [ -n "$$CLAUDE_PATH" ]; then
echo "Found Claude at $$CLAUDE_PATH"
ln -sf "$$CLAUDE_PATH/resources/native-binary/claude" /usr/local/bin/claude
else
echo "Claude extension folder not found!"
fiThe Claude Code VS Code extension bundles a precompiled native claude binary at resources/native-binary/claude inside the extension package. This is the same binary you would normally install separately via npm install -g @anthropic-ai/claude-code — bundled here, so no separate npm install step is needed at all.
The problem is that VS Code extension directories are versioned: anthropic.claude-code-1.2.3. Hardcoding the path would silently break every time you update the extension. The find with a wildcard always locates the currently installed version regardless of its version number, and head -n 1 ensures a single result even if multiple versions somehow coexist.
The conditional if [ -n "..." ] handles the bootstrap case: if the extension is not installed yet (e.g., first run before Step 2 was completed), the script skips the symlink without failing. The echo messages appear in docker compose logs code-server, making it easy to confirm the binary was found.
$$ instead of $: Docker Compose processes YAML and expands $VARIABLE as a Compose variable before the shell ever sees it — producing an empty string. $$ escapes to a literal $, so the shell receives $CLAUDE_PATH as intended. Every shell variable in this script that should not be expanded by Compose must use $$.
Startup Script Step 5 — Server Launch
exec runuser -u openvscode-server -- /home/.openvscode-server/bin/openvscode-server \
--host 0.0.0.0 \
--port 8080 \
--server-base-path /api/ingress/code_server \
--without-connection-token \
--default-folder /opt/HA \
--extensions-dir /home/workspace/.openvscode-server/extensions \
--enable-proposed-api Anthropic.claude-codeThree things here require specific explanation:
exec replaces the shell process with the server process, making the server PID 1 inside the container. Docker sends SIGTERM to PID 1 on docker stop. Without exec, the shell stays as PID 1 and may not forward the signal to the server, causing the container to be killed hard after the timeout. exec ensures clean shutdown behaviour.
runuser -u openvscode-server drops from root to the openvscode-server user for the server process. All setup steps ran as root — the server itself runs with no elevated privileges. This is the correct pattern for containers that need brief privileged initialization.
--enable-proposed-api Anthropic.claude-code is the flag that most Claude Code in code-server setups miss, and without it the extension either silently fails to load or loses nearly all its functionality. VS Code “proposed APIs” are experimental features behind a flag — disabled by default and only enabled for explicitly whitelisted extensions. Claude Code requires these proposed APIs for its agentic capabilities: reading and writing files programmatically, running terminal commands, and deep editor state access. The extension ID Anthropic.claude-code is case-sensitive and must be spelled exactly as shown.
--extensions-dir explicitly tells the server where extensions live, pointing to the separately mounted persistent volume. Without this flag, the server may fall back to an ephemeral internal path, causing extensions to disappear on recreation.
Volumes
volumes:
- /opt/HA/code-server/data:/home/workspace/.openvscode-server/data
- /opt/HA/code-server/extensions:/home/workspace/.openvscode-server/extensions
- /opt/HA:/opt/HAThree mounts with distinct purposes. data persists VS Code’s internal state: user settings, keybindings, UI layout, workspace history. extensions persists the installed extensions directory — including Claude Code — and is the directory the startup script searches at boot. Keeping them separate means you can reset one without affecting the other. The /opt/HA mount gives the editor (and Claude) access to your entire HA configuration directory — all config files, docker-compose, scripts, and the claude-config symlink target.
DNS
dns:
- YOUR_DNSReplace with your local DNS server IPs, or remove the block entirely to use Docker’s default resolver. If you run AdGuard Home or Pi-hole, pointing the container to your local resolver ensures consistent name resolution with the rest of your network. If Claude Code fails to connect to api.anthropic.com, verifying DNS resolution from inside the container is the first troubleshooting step.
Step 4 — First Start and Extension Installation
Apply the updated compose file:
docker compose up -d --remove-orphansWatch the startup sequence:
sudo docker compose logs -f code-serverYou will see this line — and it is expected on first run:
Claude extension folder not found!The startup script has an explicit if/else guard and continues without failing. The server starts fully; only the claude binary symlink is skipped. Do not restart yet — install the extension first.
Install the Claude Code Extension via CLI
The extension cannot be installed through the Extensions panel UI in this setup. The reason is that gitpod/openvscode-server uses the Open VSX registry by default — not the Microsoft VS Code Marketplace. Claude Code is published to the Microsoft marketplace and may not appear in an Open VSX search, so the UI search returns nothing.
The correct method is the openvscode-server CLI, targeting the mounted extensions directory explicitly:
sudo docker exec -u openvscode-server code-server \
/home/.openvscode-server/bin/openvscode-server \
--install-extension Anthropic.claude-code \
--extensions-dir /home/workspace/.openvscode-server/extensionsIf Open VSX does not carry the extension, you will see Extension 'Anthropic.claude-code' not found. In that case, download the .vsix package directly and install from file:
# Download the extension package
sudo docker exec -u openvscode-server code-server bash -c \
"curl -fL 'https://open-vsx.org/api/Anthropic/claude-code/latest/file/Anthropic.claude-code-latest.vsix' \
-o /tmp/claude-code.vsix"
# Install from the downloaded package into the mounted volume
sudo docker exec -u openvscode-server code-server \
/home/.openvscode-server/bin/openvscode-server \
--install-extension /tmp/claude-code.vsix \
--extensions-dir /home/workspace/.openvscode-server/extensions⚠️ Explanation
Why -u openvscode-server: The extensions directory was chowned to openvscode-server by the startup script. Installing as root would create files owned by root inside a directory owned by openvscode-server, which the server process — running as openvscode-server — cannot then read correctly. The -u flag ensures ownership is consistent.
Why --extensions-dir must be specified: Without it, the CLI tool falls back to its own internal default path — which is inside the container image, not in the mounted volume. The extension would appear to install successfully but would vanish on the next container start.
Why not “Install from VSIX” via the UI: The Extensions panel’s “Install from VSIX” option requires uploading the file through the browser, which is cumbersome and unreliable for large binary packages in an ingress-proxied setup. The CLI approach is one command and installs directly to disk.
Restart to Activate
Once the extension files are on disk, restart the container:
sudo docker compose restart code-serverWatch the startup logs again:
sudo docker compose logs -f code-serverThe output should now confirm the extension was found and the binary symlinked:
Found Claude at /home/workspace/.openvscode-server/extensions/anthropic.claude-code-X.X.X
...
[info] Extension host agent started.If the log still shows Claude extension folder not found!, confirm the extension files are actually present on the host:
ls /opt/HA/code-server/extensions/
# Expected: anthropic.claude-code-X.X.X/f the directory is empty, the install command ran against the wrong extensions path. Re-run the install command with the explicit --extensions-dir flag as shown above.
Verification
1. Confirm the navigator patch applied:
sudo docker exec code-server grep -c "userAgent.*code-server" \
/home/.openvscode-server/out/vs/workbench/api/node/extensionHostProcess.js
# Expected: 12. Confirm the claude binary is reachable as the runtime user:
sudo docker exec -u openvscode-server code-server claude --version
# Expected: claude version X.X.X3. Confirm the API key is visible inside the container:
sudo docker exec -u openvscode-server code-server sh -c 'echo $ANTHROPIC_API_KEY'
# Expected: sk-ant-...4. Confirm the Claude config directory is symlinked and has expected structure:
sudo docker exec code-server ls -la /home/workspace/.claude
# Expected: symlink → /opt/HA/code-server/claude-config
sudo docker exec -u openvscode-server code-server ls /opt/HA/code-server/claude-config
# After first Claude Code use, expected to contain: settings.json projects/ todos/5. Confirm the extension loads in the editor:
Open the HA Code-Server Ingress panel. The Claude icon should appear in the VS Code Activity Bar. Click it — the chat panel opens. Send a short test message to verify the API connection is live.
As the result you should have the beautiful setup like that:

Nginx Configuration — No Changes Required
No nginx changes are needed. Claude Code communicates outbound to api.anthropic.com over port 443 — the same outbound HTTPS any process on your server uses. There is no inbound port, no new location block, and no header modifications required. Your existing nginx configuration from the reverse proxy article handles everything. The entire Claude Code interaction is invisible to nginx — it passes through the existing ingress proxy as normal Code-Server traffic.
Summary
| Component | What it does |
|---|---|
user: "root" + runuser | Root for setup, drops to openvscode-server for the server process |
Navigator patch (sed) | Fixes Node.js 21 navigator global conflict that breaks Claude Code |
| Patch re-applied every restart | Survives docker pull of updated base image automatically |
chown at startup | Fixes volume ownership regardless of how the host created the directories |
~/.claude symlink | Persists Claude sessions, CLAUDE.md project context, conversation history, and todo state across container recreations |
Dynamic binary discovery (find) | Symlinks the bundled claude binary from the extension package; survives extension version updates |
| Extension installed via CLI | --install-extension with --extensions-dir — UI search fails because openvscode-server uses Open VSX, not the Microsoft marketplace |
--enable-proposed-api | Required flag — unlocks experimental VS Code APIs that Claude Code depends on for agentic features |
--extensions-dir | Points server to persistent volume; prevents fallback to ephemeral internal path |
exec runuser | Correct signal handling on docker stop, minimal runtime permissions |
ANTHROPIC_API_KEY in Docker env | Mandatory — only auth method on a headless server; inherited by the entire extension host process chain automatically |
You now have an AI assistant with full access to your HA configuration files, your docker-compose, and an integrated terminal — all from within the HA sidebar.

