From 9027fa9ad4510abc8ff88753c019f816c76b4b5a Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Wed, 12 Aug 2026 06:13:59 -0700 Subject: [PATCH] Stop the scheduler handing Claude root's HOME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every scheduled task failed with "Not logged in · Please run /login" while the container's OAuth credential sat there, valid, the whole time. The entrypoint snapshots the environment into ~/.claude/scheduler/.env so cron jobs get more than cron's minimal env. It runs as root, and HOME was in the capture list, so the file recorded HOME=/root. The task runner then sources that file with `set -a`, overwriting the HOME cron gave the job. `claude -p` looks for its credential under $HOME, finds no /root/.claude, and exits 1. Logging still worked — SCHEDULER_DIR is expanded before the sourcing — which is why this presents as a well-formed log of a task that never authenticated. Drop HOME from the captured set and write it explicitly instead; cron does still need one. Then restore HOME across the source in the task runner too: .env lives on the home volume, so every project created before this ships keeps a stale copy of it until its container restarts, and the runner is what has to survive that. Co-Authored-By: Claude Opus 5 (1M context) --- container/entrypoint.sh | 11 ++++++++++- container/triple-c-task-runner | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/container/entrypoint.sh b/container/entrypoint.sh index 1ce3701..1a9c0df 100644 --- a/container/entrypoint.sh +++ b/container/entrypoint.sh @@ -434,17 +434,26 @@ chown -R claude:claude "$SCHEDULER_DIR" cron # Save environment variables for cron jobs (cron runs with a minimal env) +# +# HOME is deliberately NOT captured here. This entrypoint runs as root, so the +# snapshot would record HOME=/root — and the task runner sources this file with +# `set -a`, which would overwrite the HOME cron gives the job. Claude Code then +# looks for its OAuth credential at /root/.claude/.credentials.json instead of +# /home/claude/.claude/.credentials.json and every scheduled task dies with +# "Not logged in · Please run /login". Cron still needs a HOME, so it is written +# explicitly below with the value the `claude` user actually has. ENV_FILE="$SCHEDULER_DIR/.env" : > "$ENV_FILE" env | while IFS='=' read -r key value; do case "$key" in - ANTHROPIC_*|AWS_*|CLAUDE_CODE_*|TRIPLE_C_PERMISSION_MODE|PATH|HOME|LANG|TZ|COLORTERM|BROWSER|NODE_EXTRA_CA_CERTS|REQUESTS_CA_BUNDLE|SSL_CERT_FILE) + ANTHROPIC_*|AWS_*|CLAUDE_CODE_*|TRIPLE_C_PERMISSION_MODE|PATH|LANG|TZ|COLORTERM|BROWSER|NODE_EXTRA_CA_CERTS|REQUESTS_CA_BUNDLE|SSL_CERT_FILE) # Escape single quotes in value and write as KEY='VALUE' escaped_value=$(printf '%s' "$value" | sed "s/'/'\\\\''/g") printf "%s='%s'\n" "$key" "$escaped_value" >> "$ENV_FILE" ;; esac done +printf "HOME='/home/claude'\n" >> "$ENV_FILE" chown claude:claude "$ENV_FILE" chmod 600 "$ENV_FILE" diff --git a/container/triple-c-task-runner b/container/triple-c-task-runner index e7bf701..8d50f0f 100644 --- a/container/triple-c-task-runner +++ b/container/triple-c-task-runner @@ -34,11 +34,19 @@ if ! flock -n 200; then fi # ── Source saved environment ───────────────────────────────────────────────── +# The env file is a snapshot taken by the entrypoint, which runs as root. A +# snapshot written before the entrypoint stopped capturing HOME still carries +# HOME=/root, and `set -a` would apply it to `claude` below — which then finds no +# credential under /root/.claude and exits with "Not logged in". The env file +# lives on the home volume, so those stale copies outlive an image update until +# the container is restarted; keep our own HOME regardless of what it says. if [ -f "$ENV_FILE" ]; then + REAL_HOME="${HOME:-/home/claude}" set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a + HOME="$REAL_HOME" fi # ── Read task definition ────────────────────────────────────────────────────