From c1e660a2fd6959fe483c581d23b8f4e638a314f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 13:41:16 +0000 Subject: [PATCH] feat(cloud): sign in with a network secret instead of SHELLTIME_TOKEN The SessionStart hook now signs in with the placeholder token proxy-injected, so the ShellTime token can live in the environment's network secrets, where sessions never see it and api.shelltime.xyz needs no network access setting. SHELLTIME_TOKEN still wins when set, for environments without network secrets. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VF4QMGS5ymib7ntoJAhbUB --- .github/workflows/test.yaml | 17 ++++++++++++++++- CLAUDE.md | 4 ++-- README.md | 2 +- cloud.bash | 31 +++++++++++++++++++------------ 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 93ffe00..6c9913d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -108,6 +108,21 @@ jobs: jq -e '.env.OTEL_EXPORTER_OTLP_ENDPOINT' "$settings" count=$(jq --arg cmd "$hook" '[.hooks.SessionStart[].hooks[] | select(.command == $cmd)] | length' "$settings") [ "$count" = 1 ] || { echo "expected one SessionStart hook, found $count"; exit 1; } - # Without SHELLTIME_TOKEN the hook does nothing and prints nothing + config="$HOME/.shelltime/config.yaml" + daemon="$HOME/.shelltime/bin/shelltime-daemon" + # The hook prints nothing: a SessionStart hook's output goes into Claude's context + # Without SHELLTIME_TOKEN it signs in with the network secret's placeholder out=$(env -u SHELLTIME_TOKEN "$hook") [ -z "$out" ] || { echo "hook printed: $out"; exit 1; } + grep -qx "token: proxy-injected" "$config" || { echo "expected the proxy-injected token"; cat "$config"; exit 1; } + # and starts the daemon, once + for _ in 1 2 3 4 5; do pgrep -fx "$daemon" >/dev/null && break; sleep 1; done + pgrep -fx "$daemon" >/dev/null || { echo "daemon not started"; cat "$HOME/.shelltime/logs/shelltime-daemon.log"; exit 1; } + # SHELLTIME_TOKEN takes the placeholder's place, and back again once it's gone + out=$(SHELLTIME_TOKEN=st_v1_ci_token "$hook") + [ -z "$out" ] || { echo "hook printed: $out"; exit 1; } + grep -qx "token: st_v1_ci_token" "$config" || { echo "expected SHELLTIME_TOKEN"; cat "$config"; exit 1; } + env -u SHELLTIME_TOKEN "$hook" + grep -qx "token: proxy-injected" "$config" || { echo "expected the proxy-injected token again"; cat "$config"; exit 1; } + [ "$(pgrep -fxc "$daemon")" = 1 ] || { echo "expected one daemon"; pgrep -fxa "$daemon"; exit 1; } + pkill -fx "$daemon" diff --git a/CLAUDE.md b/CLAUDE.md index ddc1f68..3c02ff9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ This is the **shelltime installation** repository - a collection of shell script ## Repository Structure - `install.bash` - Main installation script that downloads shelltime CLI binaries and sets up shell hooks -- `cloud.bash` - Setup for Claude Code cloud environments, run from an environment's setup script with `curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash`. Installs the Linux CLI and daemon into `~/.shelltime/bin`, runs `shelltime cc install`, and adds a Claude Code SessionStart hook (`~/.shelltime/bin/cloud-session-start.sh`, embedded in the script) that signs in with `SHELLTIME_TOKEN` and starts the daemon in each session. It installs nothing unless `CLAUDE_CODE_REMOTE` is `true`, which cloud environments set (the setup script inherits it from `environment-manager`) and which is never true on a user's own machine. A setup script that exits non-zero stops sessions from starting, so it always exits 0 (its `EXIT` trap reports a failure instead), and nothing runs before the final `main` call, so a truncated download installs nothing. The guide that documents it is `content/blogs/track-claude-code-cloud-sessions/` in the web repo; keep the two in step +- `cloud.bash` - Setup for Claude Code cloud environments, run from an environment's setup script with `curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash`. Installs the Linux CLI and daemon into `~/.shelltime/bin`, runs `shelltime cc install`, and adds a Claude Code SessionStart hook (`~/.shelltime/bin/cloud-session-start.sh`, embedded in the script) that signs in and starts the daemon in each session. The token is a network secret of the environment (host `api.shelltime.xyz`, header `Authorization`, prefix `CLI`) that Anthropic's agent proxy adds to requests outside the VM, so the hook signs in with the placeholder `proxy-injected` (the CLI then sends no `Authorization` header; needs a CLI release with `model.ProxyInjectedToken`). `SHELLTIME_TOKEN` in the environment variables, for environments without network secrets, replaces the placeholder. Setup scripts get neither. It installs nothing unless `CLAUDE_CODE_REMOTE` is `true`, which cloud environments set (the setup script inherits it from `environment-manager`) and which is never true on a user's own machine. A setup script that exits non-zero stops sessions from starting, so it always exits 0 (its `EXIT` trap reports a failure instead), and nothing runs before the final `main` call, so a truncated download installs nothing. The guide that documents it is `content/blogs/track-claude-code-cloud-sessions/` in the web repo; keep the two in step - `hooks/` - Shell-specific hook scripts that track command execution: - `zsh.zsh` - Uses zsh's native `preexec` and `precmd` hooks - `bash.bash` - Uses `bash-preexec` library for preexec/precmd functionality @@ -29,7 +29,7 @@ The CI verifies: 2. Shell configuration files are properly sourced 3. Hook files are placed in `~/.shelltime/hooks/` -The `test-cloud` job checks that `cloud.bash` installs nothing without `CLAUDE_CODE_REMOTE=true`, then runs it twice with it on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, that the SessionStart hook is registered exactly once, and that the hook prints nothing without `SHELLTIME_TOKEN`. +The `test-cloud` job checks that `cloud.bash` installs nothing without `CLAUDE_CODE_REMOTE=true`, then runs it twice with it on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, that the SessionStart hook is registered exactly once, and that the hook prints nothing, signs in with `proxy-injected` (or `SHELLTIME_TOKEN` when set) and starts one daemon. ## How Shell Hooks Work diff --git a/README.md b/README.md index 5894a73..6dc0747 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To track Claude Code sessions that run in the cloud ([claude.ai/code](https://cl curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash ``` -The environment also needs your ShellTime API token as `SHELLTIME_TOKEN` in its environment variables, and `api.shelltime.xyz` allowed by its network access. [cloud.bash](cloud.bash) installs the CLI and daemon, connects Claude Code, and adds a SessionStart hook that signs in with the token and starts the daemon in every session. It never fails the setup script, and outside a cloud environment (where `CLAUDE_CODE_REMOTE` isn't `true`) it installs nothing. The full guide: [Track your Claude Code cloud sessions](https://shelltime.xyz/blogs/track-claude-code-cloud-sessions). +Then add your ShellTime API token to the environment as a network secret: allowed website `api.shelltime.xyz`, header `Authorization`, prefix `CLI`. Anthropic's agent proxy adds it to the daemon's requests outside the session's VM, so sessions never see the token, and the host needs no network access setting. [cloud.bash](cloud.bash) installs the CLI and daemon, connects Claude Code, and adds a SessionStart hook that signs in and starts the daemon in every session. An environment without network secrets can hold the token as `SHELLTIME_TOKEN` in its environment variables instead, with `api.shelltime.xyz` allowed by its network access; the hook uses it when it's set. It never fails the setup script, and outside a cloud environment (where `CLAUDE_CODE_REMOTE` isn't `true`) it installs nothing. The full guide: [Track your Claude Code cloud sessions](https://shelltime.xyz/blogs/track-claude-code-cloud-sessions). ## What Next? diff --git a/cloud.bash b/cloud.bash index e58f290..73a9b9b 100755 --- a/cloud.bash +++ b/cloud.bash @@ -6,11 +6,15 @@ # # curl -fsSL https://raw.githubusercontent.com/shelltime/installation/main/cloud.bash | bash # -# The environment also needs SHELLTIME_TOKEN in its environment variables and -# api.shelltime.xyz allowed by its network access. Setup scripts don't see the -# environment's variables, so this only installs; a SessionStart hook signs in -# with the token in every session. Anywhere CLAUDE_CODE_REMOTE isn't true, it -# installs nothing. +# Then add your ShellTime API token to the environment as a network secret for +# api.shelltime.xyz, sent in the Authorization header with the prefix CLI. +# Anthropic's agent proxy adds it to the daemon's requests after they leave the +# session's VM, so the token is never in the VM, and the secret's host needs no +# network access setting. An environment without network secrets can hold the +# token in SHELLTIME_TOKEN among its environment variables instead, with +# api.shelltime.xyz allowed by its network access. Setup scripts get neither, +# so this only installs; a SessionStart hook signs in and starts the daemon in +# every session. Anywhere CLAUDE_CODE_REMOTE isn't true, it installs nothing. # # Guide: https://shelltime.xyz/blogs/track-claude-code-cloud-sessions @@ -59,18 +63,21 @@ connect_claude_code() { } # 3. The snapshot keeps files but no processes, there is no systemd, and only -# the session sees SHELLTIME_TOKEN, so a SessionStart hook signs in and starts -# the daemon in every session +# sessions see SHELLTIME_TOKEN, so a SessionStart hook signs in and starts the +# daemon in every session install_session_hook() { cat >"$HOOK_PATH" <<'EOF' #!/bin/bash # Claude Code SessionStart hook, installed by ShellTime's cloud.bash. Its stdout # would go into Claude's context, so it prints nothing. st="$HOME/.shelltime" -[ -n "${SHELLTIME_TOKEN:-}" ] || exit 0 -# The first session, or a token changed in the environment settings -if ! grep -qF "$SHELLTIME_TOKEN" "$st/config.yaml" 2>/dev/null; then - "$st/bin/shelltime" auth --token "$SHELLTIME_TOKEN" >/dev/null 2>&1 || exit 0 +# With the token in a network secret, the CLI signs in with the placeholder +# proxy-injected and sends no Authorization header; the agent proxy adds the +# real one. SHELLTIME_TOKEN, for environments without network secrets, wins. +token="${SHELLTIME_TOKEN:-proxy-injected}" +# The first session, or the token changed in the environment settings +if ! grep -qF "$token" "$st/config.yaml" 2>/dev/null; then + "$st/bin/shelltime" auth --token "$token" >/dev/null 2>&1 || exit 0 fi if ! pgrep -fx "$st/bin/shelltime-daemon" >/dev/null; then setsid nohup "$st/bin/shelltime-daemon" >>"$st/logs/shelltime-daemon.log" 2>&1