Fetching latest headlines…
Claude Code on the Web: Why Your .env Vars Don't Reach the Setup Script (and How SessionStart Hook Fixes It)
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’April 19, 2026

Claude Code on the Web: Why Your .env Vars Don't Reach the Setup Script (and How SessionStart Hook Fixes It)

2 views0 likes0 comments
Originally published byDev.to

TL;DR

  • Environment variables you put in the .env panel of Claude Code on the web (Cloud Sandbox) do not reach the setup script.
  • They only reach the shell inside the running Claude Code session.
  • So git clone "https://x-access-token:${GH_TOKEN}@..." inside the setup script fails β€” GH_TOKEN is empty at that point.
  • Move the clone into a SessionStart hook and it just works, because by then $GH_TOKEN is populated.

This is a write-up of how I narrowed down a behavior that the official docs don't spell out clearly.

Background

What I wanted to do

I've been collecting custom slash commands, skills, and a personal CLAUDE.md under ~/.claude/ locally. I wanted the same setup available inside Claude Code on the web. Everything is stored in a private repo called miyashita337/agent-base.

According to the docs, cloud sessions do not carry over user-level settings like ~/.claude/CLAUDE.md β€” only what's committed to the repo is available. So my plan was: on session start, clone agent-base and symlink its contents into ~/.claude/.

First attempt (failed)

I put a GitHub PAT into the .env panel of the Cloud Sandbox settings and tried to clone from the setup script.

# setup script
#!/bin/bash
set -e
git clone "https://x-access-token:${GH_TOKEN}@github.com/miyashita337/agent-base.git" "$HOME/agent-base"

.env panel:

GH_TOKEN=github_pat_...   # ~90 chars, real value
GIT_AUTHOR_NAME=miyashita337
...

Result: fatal: could not read Username.

Narrowing it down

Symptom 1: my echo output never showed up

For debugging I added echo "GH_TOKEN length: ${#GH_TOKEN}" in the setup script. But the setup-script UI only shows the last few lines of output, so anything mid-script gets silently dropped.

Workaround: dump everything to a log file

I rewrote the script to redirect diagnostics into /tmp/env-diag.log and then cat it from inside the session.

#!/bin/bash
set -e

LOG=/tmp/env-diag.log

{
  echo "===== ENV DIAGNOSTICS ====="
  echo "GH_TOKEN length: ${#GH_TOKEN}"
  echo "GIT_AUTHOR_NAME: [${GIT_AUTHOR_NAME}]"
  echo "TZ: [${TZ}]"
  echo "LANG: [${LANG}]"
  echo ""
  echo "--- All env vars (names only) ---"
  env | cut -d= -f1 | sort
} | tee "$LOG"

Then I started a new session and asked Claude Code to cat /tmp/env-diag.log.

Symptom 2: the cache trap

On some runs I didn't even see the "setup script executed" log line. Per the docs, the setup script runs only on first creation β€” after that, a filesystem snapshot is reused. Editing the .env panel alone does not invalidate the snapshot.

What does invalidate it:

  • Changing the setup script body
  • Changing the allowed-domains list
  • ~7 days of age

So I added a throwaway comment line like # cache-bust 2026-04-19-01 at the top of the setup script. Semantically it's a no-op, but it's enough to force a rerun on the next session.

The result: every env var was empty

===== ENV DIAGNOSTICS =====
GH_TOKEN length: 0
GIT_AUTHOR_NAME: []
TZ: []
LANG: []
...
Custom vars found: 0 / expected 8

It wasn't just GH_TOKEN β€” nothing from the .env panel was reaching the setup script.

Diagnostic log: 0 / 8 expected custom vars found

The clincher: the session shell has them

Inside the running Claude Code session, I checked the same variables directly from the shell:

$ echo "GH=${#GH_TOKEN}, TZ=[$TZ], LANG=[$LANG]"
GH=93, TZ=[Asia/Tokyo], LANG=[ja_JP.UTF-8]

Shell output showing GH=93, TZ=[Asia/Tokyo], LANG=[ja_JP.UTF-8]

All present. So:

When it runs .env panel vars available?
Setup script ❌ No
Claude Code session shell βœ… Yes

Root cause and fix

Root cause

The .env panel is injected only into the Claude Code session shell, not into the setup script. It's not a bug β€” it's just not documented clearly. The docs show GH_TOKEN as an example .env value, but that's aimed at the gh CLI picking it up inside the session, not at setup-script usage.

Fix: move clone logic into a SessionStart hook

Drop the clone from the setup script. Put it in a SessionStart hook defined in the repo's .claude/settings.json. The hook runs after Claude Code has started, so $GH_TOKEN is in scope.

.claude/settings.json

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/setup-agent-base.sh"
          }
        ]
      }
    ]
  }
}

scripts/setup-agent-base.sh

#!/bin/bash
set -e

AGENT_BASE_DIR="$HOME/agent-base"

# Skip locally
[ "$CLAUDE_CODE_REMOTE" != "true" ] && exit 0

# Clone (GH_TOKEN is available at this point)
if [ ! -d "$AGENT_BASE_DIR" ]; then
  if [ -z "${GH_TOKEN:-}" ]; then
    echo "setup-agent-base: GH_TOKEN is not set" >&2
    exit 1
  fi
  # Pass the token via extraHeader so it doesn't end up in .git/config
  git -c http.extraHeader="Authorization: Bearer ${GH_TOKEN}" \
      clone "https://github.com/miyashita337/agent-base.git" "$AGENT_BASE_DIR"
  git -C "$AGENT_BASE_DIR" config --unset-all http.extraHeader 2>/dev/null || true
fi

# Symlink into ~/.claude/ (idempotent)
mkdir -p "$HOME/.claude"
for dir in commands skills agents hooks; do
  src="$AGENT_BASE_DIR/$dir"
  dst="$HOME/.claude/$dir"
  if [ -d "$src" ]; then
    # If the destination is a real directory, back it up first
    # (ln -sf into an existing dir creates a nested symlink inside it)
    if [ -d "$dst" ] && [ ! -L "$dst" ]; then
      mv "$dst" "${dst}.bak.$(date +%s)"
    fi
    ln -sfn "$src" "$dst"
  fi
done

if [ -f "$AGENT_BASE_DIR/CLAUDE.md" ]; then
  ln -sf "$AGENT_BASE_DIR/CLAUDE.md" "$HOME/.claude/CLAUDE.md"
fi

exit 0

A few deliberate choices:

  1. CLAUDE_CODE_REMOTE guard β€” early-exit outside of cloud sessions so local dev isn't affected.
  2. Idempotent β€” skip clone if the dir exists, but always refresh the symlinks. Partial-failure recovery just works.
  3. No token in the URL β€” use http.extraHeader so .git/config never contains a plaintext token.
  4. ln -sfn not ln -sf β€” if the destination is already a real directory, ln -sf nests the symlink inside it (e.g. ~/.claude/commands/commands). Backing it up first and using -n (--no-dereference) forces a clean replacement.

What about the setup script?

Leave it empty. You can delete the diagnostics too.

Verification

Fresh session, ls -la ~/.claude/:

CLAUDE.md -> /home/user/agent-base/CLAUDE.md
commands  -> /home/user/agent-base/commands
skills    -> /home/user/agent-base/skills
agents    -> /home/user/agent-base/agents
hooks     -> /home/user/agent-base/hooks

Typing / shows all the custom slash commands from agent-base (/capture, /pdca, /inv, ...) and they execute without issue.

Gotchas summary

Gotcha Fix
.env vars don't reach the setup script Move clone into a SessionStart hook
Setup-script echo output is truncated Redirect to a log file and cat it later
Setup script is cached and won't rerun Add/edit a throwaway comment to bust the cache
New sessions can't start with an empty prompt Type anything β€” but remember it becomes the first instruction to Claude
ln -sf doesn't overwrite existing directories Back up first, then ln -sfn
git clone https://x-access-token:${TOKEN}@... leaks the token into .git/config Pass it via -c http.extraHeader=... instead

Closing

The "setup script can't see .env vars" behavior is inferable if you read the docs carefully β€” the gh CLI example hints at "this is for in-session auto-pickup" β€” but it's never stated plainly. Easy to misread as "you can use these in the setup script too."

Hope this saves someone else the afternoon I lost.

References

Comments (0)

Sign in to join the discussion

Be the first to comment!