From e45faf400ff1d3f9d79e4b081836fd855fc3f769 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 10:47:52 -0600 Subject: [PATCH 01/20] feat: initial session refactor --- .../coder/modules/claude-code/main.test.ts | 137 ++++++++++++++++ registry/coder/modules/claude-code/main.tf | 13 +- .../scripts/remove-last-session-id.sh | 44 ----- .../modules/claude-code/scripts/start.sh | 154 +++++++++--------- 4 files changed, 217 insertions(+), 131 deletions(-) delete mode 100755 registry/coder/modules/claude-code/scripts/remove-last-session-id.sh diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 94fcb391a..b513b1d40 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -360,4 +360,141 @@ describe("claude-code", async () => { "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", ); }); + + test("partial-initialization-detection", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + // Create a partial/invalid session file + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + + // Write incomplete session (only 1 line, should fail validation) + await execContainer(id, [ + "bash", + "-c", + `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/session-${taskSessionId}.jsonl`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should start new session, not try to resume invalid one + expect(startLog.stdout).toContain("Starting new task session"); + expect(startLog.stdout).toContain("--session-id"); + }); + + test("standalone-first-build-no-sessions", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + }, + }); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should start fresh, not try to continue + expect(startLog.stdout).toContain("No sessions found"); + expect(startLog.stdout).toContain("starting fresh standalone session"); + expect(startLog.stdout).not.toContain("--continue"); + }); + + test("standalone-with-sessions-continues", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + }, + }); + + // Create a generic session file + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/session-generic-123.jsonl << 'EOF' +{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2025-12-02T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T10:00:05.000Z"} +EOF`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should continue existing session + expect(startLog.stdout).toContain("Sessions found"); + expect(startLog.stdout).toContain("Continuing most recent standalone session"); + expect(startLog.stdout).toContain("--continue"); + }); + + test("task-mode-ignores-manual-sessions", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + + // Create task session + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/session-${taskSessionId}.jsonl << 'EOF' +{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2025-12-02T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T10:00:05.000Z"} +EOF`, + ]); + + // Create manual session (newer) + await execContainer(id, [ + "bash", + "-c", + `sleep 1 && cat > ${sessionDir}/session-manual-456.jsonl << 'EOF' +{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2025-12-02T12:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T12:00:05.000Z"} +EOF`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should resume task session, not manual session + expect(startLog.stdout).toContain("Resuming task session"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).not.toContain("manual-456"); + }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index a994d9cbe..db24493b5 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -291,12 +291,11 @@ resource "coder_env" "disable_autoupdater" { locals { # we have to trim the slash because otherwise coder exp mcp will # set up an invalid claude config - workdir = trimsuffix(var.workdir, "/") - app_slug = "ccw" - install_script = file("${path.module}/scripts/install.sh") - start_script = file("${path.module}/scripts/start.sh") - module_dir_name = ".claude-module" - remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh")) + workdir = trimsuffix(var.workdir, "/") + app_slug = "ccw" + install_script = file("${path.module}/scripts/install.sh") + start_script = file("${path.module}/scripts/start.sh") + module_dir_name = ".claude-module" # Extract hostname from access_url for boundary --allow flag coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") @@ -357,9 +356,7 @@ module "agentapi" { set -o errexit set -o pipefail echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh - echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh" chmod +x /tmp/start.sh - chmod +x /tmp/remove-last-session-id.sh ARG_MODEL='${var.model}' \ ARG_RESUME_SESSION_ID='${var.resume_session_id}' \ diff --git a/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh b/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh deleted file mode 100755 index d72369fa7..000000000 --- a/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh +++ /dev/null @@ -1,44 +0,0 @@ -# If lastSessionId is present in .claude.json, claude --continue will start a -# conversation starting from that session. The problem is that lastSessionId -# doesn't always point to the last session. The field is updated by claude only -# at the point of normal CLI exit. If Claude exits with an error, or if the user -# restarts the Coder workspace, lastSessionId will be stale, and claude --continue -# will start from an old session. -# -# If lastSessionId is missing, claude seems to accurately figure out where to -# start using the conversation history - even if the CLI previously exited with -# an error. -# -# This script removes the lastSessionId field from .claude.json. -if [ $# -eq 0 ]; then - echo "No working directory provided - it must be the first argument" - exit 1 -fi - -# Get absolute path of working directory -working_dir=$(realpath "$1") -echo "workingDir $working_dir" - -# Path to .claude.json -claude_json_path="$HOME/.claude.json" -echo ".claude.json path $claude_json_path" - -# Check if .claude.json exists -if [ ! -f "$claude_json_path" ]; then - echo "No .claude.json file found" - exit 1 -fi - -# Use jq to check if lastSessionId exists for the working directory and remove it - -if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then - # Remove lastSessionId and update the file - if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then - echo "Removed lastSessionId from .claude.json" - exit 0 - else - echo "Failed to remove lastSessionId from .claude.json" - fi -else - echo "No lastSessionId found in .claude.json - nothing to do" -fi diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 59983b9e8..074667cff 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -51,19 +51,6 @@ printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" echo "--------------------------------" -# Clean up stale session data (see remove-last-session-id.sh for details) -CAN_CONTINUE_CONVERSATION=false -set +e -bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null -session_cleanup_exit_code=$? -set -e - -case $session_cleanup_exit_code in - 0) - CAN_CONTINUE_CONVERSATION=true - ;; -esac - function install_boundary() { if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then # Install boundary by compiling from source @@ -100,17 +87,59 @@ function validate_claude_installation() { TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" task_session_exists() { - local workdir_normalized - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') local project_dir="$HOME/.claude/projects/${workdir_normalized}" + local session_file="$project_dir/session-${TASK_SESSION_ID}.jsonl" + + if [ -f "$session_file" ]; then + printf "Task session file found: %s\n" "$session_file" + return 0 + else + printf "Task session file not found: %s\n" "$session_file" + return 1 + fi +} - printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized" +is_valid_session() { + local session_file="$1" + + if [ ! -f "$session_file" ] || [ ! -s "$session_file" ]; then + printf "Session validation failed: file missing or empty\n" + return 1 + fi + + if ! jq -e '.sessionId' < <(head -1 "$session_file") >/dev/null 2>&1; then + printf "Session validation failed: invalid JSONL format\n" + return 1 + fi + + local line_count=$(wc -l < "$session_file") + if [ "$line_count" -lt 2 ]; then + printf "Session validation failed: incomplete (only %d lines)\n" "$line_count" + return 1 + fi + + local mod_time=$(stat -c %Y "$session_file" 2>/dev/null || echo 0) + local now=$(date +%s) + local age=$((now - mod_time)) + if [ "$age" -lt 2 ]; then + printf "Session validation failed: file being written (age: %d seconds)\n" "$age" + return 1 + fi + + printf "Session validation passed: %s\n" "$session_file" + return 0 +} - if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then - printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID" +has_any_sessions() { + local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + local project_dir="$HOME/.claude/projects/${workdir_normalized}" + + if [ -d "$project_dir" ] && find "$project_dir" -name "*.jsonl" 2>/dev/null | grep -q .; then + printf "Sessions found in: %s\n" "$project_dir" return 0 else - printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID" + printf "No sessions found in: %s\n" "$project_dir" return 1 fi } @@ -133,75 +162,42 @@ function start_agentapi() { fi if [ -n "$ARG_RESUME_SESSION_ID" ]; then - echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID" + echo "Resuming specified session: $ARG_RESUME_SESSION_ID" ARGS+=(--resume "$ARG_RESUME_SESSION_ID") - if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions) - fi + [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) + elif [ "$ARG_CONTINUE" = "true" ]; then - if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then - echo "Task session detected (ID: $TASK_SESSION_ID)" - ARGS+=(--resume "$TASK_SESSION_ID") - ARGS+=(--dangerously-skip-permissions) - echo "Resuming existing task session" - elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then - echo "Previous session exists" - ARGS+=(--continue) - if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions) + + if [ "$ARG_REPORT_TASKS" = "true" ]; then + local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + local project_dir="$HOME/.claude/projects/${workdir_normalized}" + local session_file="$project_dir/session-${TASK_SESSION_ID}.jsonl" + + if task_session_exists && is_valid_session "$session_file"; then + echo "Resuming task session: $TASK_SESSION_ID" + ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions) + else + echo "Starting new task session: $TASK_SESSION_ID" + ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions) + [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi - echo "Resuming existing session" + else - echo "No existing session found" - if [ "$ARG_REPORT_TASKS" = "true" ]; then - if task_session_exists; then - ARGS+=(--resume "$TASK_SESSION_ID") - else - ARGS+=(--session-id "$TASK_SESSION_ID") - fi - fi - if [ -n "$ARG_AI_PROMPT" ]; then - if [ "$ARG_REPORT_TASKS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT") - else - if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions) - fi - ARGS+=(-- "$ARG_AI_PROMPT") - fi - echo "Starting new session with prompt" + if has_any_sessions; then + echo "Continuing most recent standalone session" + ARGS+=(--continue) + [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) else - if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions) - fi - echo "Starting new session" + echo "No sessions found, starting fresh standalone session" + [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") + [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) fi fi + else echo "Continue disabled, starting fresh session" - if [ "$ARG_REPORT_TASKS" = "true" ]; then - if task_session_exists; then - ARGS+=(--resume "$TASK_SESSION_ID") - else - ARGS+=(--session-id "$TASK_SESSION_ID") - fi - fi - if [ -n "$ARG_AI_PROMPT" ]; then - if [ "$ARG_REPORT_TASKS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT") - else - if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions) - fi - ARGS+=(-- "$ARG_AI_PROMPT") - fi - echo "Starting new session with prompt" - else - if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then - ARGS+=(--dangerously-skip-permissions) - fi - echo "Starting claude code session" - fi + [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") + [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) fi printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" From 76a9b750846dcf6117ba8b37ffc416f47fae14c3 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 11:46:57 -0600 Subject: [PATCH 02/20] refactor(claude-code): update session file naming conventions and improve test cases --- .../coder/modules/claude-code/main.test.ts | 21 ++++++++++--------- .../modules/claude-code/scripts/start.sh | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index b513b1d40..ae6152cc8 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -208,13 +208,17 @@ describe("claude-code", async () => { }); // Create a mock task session file with the hardcoded task session ID + // Note: Claude CLI creates files without "session-" prefix when using --session-id const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; await execContainer(id, ["mkdir", "-p", sessionDir]); await execContainer(id, [ "bash", "-c", - `touch ${sessionDir}/session-${taskSessionId}.jsonl`, + `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' +{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2025-12-03T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-03T10:00:05.000Z"} +SESSIONEOF`, ]); await execModuleScript(id); @@ -226,7 +230,7 @@ describe("claude-code", async () => { ]); expect(startLog.stdout).toContain("--resume"); expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).toContain("Resuming existing task session"); + expect(startLog.stdout).toContain("Resuming task session"); expect(startLog.stdout).toContain("--dangerously-skip-permissions"); }); @@ -370,16 +374,14 @@ describe("claude-code", async () => { }, }); - // Create a partial/invalid session file const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; await execContainer(id, ["mkdir", "-p", sessionDir]); - // Write incomplete session (only 1 line, should fail validation) await execContainer(id, [ "bash", "-c", - `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/session-${taskSessionId}.jsonl`, + `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, ]); await execModuleScript(id); @@ -425,13 +427,12 @@ describe("claude-code", async () => { }, }); - // Create a generic session file const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; await execContainer(id, ["mkdir", "-p", sessionDir]); await execContainer(id, [ "bash", "-c", - `cat > ${sessionDir}/session-generic-123.jsonl << 'EOF' + `cat > ${sessionDir}/generic-123.jsonl << 'EOF' {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2025-12-02T10:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T10:00:05.000Z"} EOF`, @@ -464,11 +465,11 @@ EOF`, const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; await execContainer(id, ["mkdir", "-p", sessionDir]); - // Create task session + // Create task session (without "session-" prefix, as CLI does) await execContainer(id, [ "bash", "-c", - `cat > ${sessionDir}/session-${taskSessionId}.jsonl << 'EOF' + `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2025-12-02T10:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T10:00:05.000Z"} EOF`, @@ -478,7 +479,7 @@ EOF`, await execContainer(id, [ "bash", "-c", - `sleep 1 && cat > ${sessionDir}/session-manual-456.jsonl << 'EOF' + `sleep 1 && cat > ${sessionDir}/manual-456.jsonl << 'EOF' {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2025-12-02T12:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T12:00:05.000Z"} EOF`, diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 074667cff..acf352369 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -89,7 +89,7 @@ TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" task_session_exists() { local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') local project_dir="$HOME/.claude/projects/${workdir_normalized}" - local session_file="$project_dir/session-${TASK_SESSION_ID}.jsonl" + local session_file="$project_dir/${TASK_SESSION_ID}.jsonl" if [ -f "$session_file" ]; then printf "Task session file found: %s\n" "$session_file" @@ -171,7 +171,7 @@ function start_agentapi() { if [ "$ARG_REPORT_TASKS" = "true" ]; then local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') local project_dir="$HOME/.claude/projects/${workdir_normalized}" - local session_file="$project_dir/session-${TASK_SESSION_ID}.jsonl" + local session_file="$project_dir/${TASK_SESSION_ID}.jsonl" if task_session_exists && is_valid_session "$session_file"; then echo "Resuming task session: $TASK_SESSION_ID" From 289f3b174ac89df3157cb4f91fb2d34c0a88f58f Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 12:16:34 -0600 Subject: [PATCH 03/20] refactor(claude-code): correct session validation logic in start.sh --- registry/coder/modules/claude-code/scripts/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index acf352369..23aa66a55 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -108,7 +108,7 @@ is_valid_session() { return 1 fi - if ! jq -e '.sessionId' < <(head -1 "$session_file") >/dev/null 2>&1; then + if ! head -3 "$session_file" | jq empty 2>/dev/null; then printf "Session validation failed: invalid JSONL format\n" return 1 fi From bbaba7f60f8bc92d53b5a940dc3dbef7590ea896 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 13:46:08 -0600 Subject: [PATCH 04/20] chore(claude-code): cleanup README and bump version --- registry/coder/modules/claude-code/README.md | 23 ++++++------ .../coder/modules/claude-code/main.test.ts | 36 ------------------- 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index b467bc3bf..3ff2d690a 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.2" + version = "4.2.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,13 +45,13 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "dev.registry.coder.com/coder/claude-code/coder" + version = "4.2.3" enable_boundary = true - boundary_version = "4.2.2" + boundary_version = "main" boundary_log_dir = "/tmp/boundary_logs" boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "4.2.2" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.2" + version = "4.2.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -78,8 +78,8 @@ module "claude-code" { # OR claude_code_oauth_token = "xxxxx-xxxx-xxxx" - claude_code_version = "4.2.2" # Pin to a specific version - agentapi_version = "4.2.2" + claude_code_version = "2.0.57" # Pin to a specific version + agentapi_version = "v0.10.0" ai_prompt = data.coder_parameter.ai_prompt.value model = "sonnet" @@ -106,13 +106,12 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.2" + version = "4.2.3" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true - claude_code_version = "4.2.2" + claude_code_version = "latest" report_tasks = false - cli_app = true } ``` @@ -129,7 +128,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.2" + version = "4.2.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +201,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.2" + version = "4.2.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +258,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.2" + version = "4.2.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index ae6152cc8..e9f377e80 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -234,42 +234,6 @@ SESSIONEOF`, expect(startLog.stdout).toContain("--dangerously-skip-permissions"); }); - test("claude-continue-resume-standalone-session", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - ai_prompt: "test prompt", - }, - }); - - const sessionId = "some-random-session-id"; - const workdir = "/home/coder/project"; - const claudeJson = { - projects: { - [workdir]: { - lastSessionId: sessionId, - }, - }, - }; - - await execContainer(id, [ - "bash", - "-c", - `echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain("--continue"); - expect(startLog.stdout).toContain("Resuming existing session"); - }); - test("pre-post-install-scripts", async () => { const { id } = await setup({ moduleVariables: { From ff08d721fad9f972917faa5f40429301c4f117af Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 14:11:13 -0600 Subject: [PATCH 05/20] refactor(claude-code): improve session validation in tests and scripts --- .../coder/modules/claude-code/main.test.ts | 6 ++-- .../modules/claude-code/scripts/start.sh | 34 +++++++------------ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index e9f377e80..1ed1699f9 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -412,7 +412,9 @@ EOF`, // Should continue existing session expect(startLog.stdout).toContain("Sessions found"); - expect(startLog.stdout).toContain("Continuing most recent standalone session"); + expect(startLog.stdout).toContain( + "Continuing most recent standalone session", + ); expect(startLog.stdout).toContain("--continue"); }); @@ -443,7 +445,7 @@ EOF`, await execContainer(id, [ "bash", "-c", - `sleep 1 && cat > ${sessionDir}/manual-456.jsonl << 'EOF' + `cat > ${sessionDir}/manual-456.jsonl << 'EOF' {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2025-12-02T12:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T12:00:05.000Z"} EOF`, diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 23aa66a55..3c86477e6 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -90,7 +90,7 @@ task_session_exists() { local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') local project_dir="$HOME/.claude/projects/${workdir_normalized}" local session_file="$project_dir/${TASK_SESSION_ID}.jsonl" - + if [ -f "$session_file" ]; then printf "Task session file found: %s\n" "$session_file" return 0 @@ -102,31 +102,23 @@ task_session_exists() { is_valid_session() { local session_file="$1" - + if [ ! -f "$session_file" ] || [ ! -s "$session_file" ]; then printf "Session validation failed: file missing or empty\n" return 1 fi - - if ! head -3 "$session_file" | jq empty 2>/dev/null; then + + if ! head -3 "$session_file" | jq empty 2> /dev/null; then printf "Session validation failed: invalid JSONL format\n" return 1 fi - + local line_count=$(wc -l < "$session_file") if [ "$line_count" -lt 2 ]; then printf "Session validation failed: incomplete (only %d lines)\n" "$line_count" return 1 fi - - local mod_time=$(stat -c %Y "$session_file" 2>/dev/null || echo 0) - local now=$(date +%s) - local age=$((now - mod_time)) - if [ "$age" -lt 2 ]; then - printf "Session validation failed: file being written (age: %d seconds)\n" "$age" - return 1 - fi - + printf "Session validation passed: %s\n" "$session_file" return 0 } @@ -134,8 +126,8 @@ is_valid_session() { has_any_sessions() { local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') local project_dir="$HOME/.claude/projects/${workdir_normalized}" - - if [ -d "$project_dir" ] && find "$project_dir" -name "*.jsonl" 2>/dev/null | grep -q .; then + + if [ -d "$project_dir" ] && find "$project_dir" -name "*.jsonl" 2> /dev/null | grep -q .; then printf "Sessions found in: %s\n" "$project_dir" return 0 else @@ -165,14 +157,14 @@ function start_agentapi() { echo "Resuming specified session: $ARG_RESUME_SESSION_ID" ARGS+=(--resume "$ARG_RESUME_SESSION_ID") [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - + elif [ "$ARG_CONTINUE" = "true" ]; then - + if [ "$ARG_REPORT_TASKS" = "true" ]; then local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') local project_dir="$HOME/.claude/projects/${workdir_normalized}" local session_file="$project_dir/${TASK_SESSION_ID}.jsonl" - + if task_session_exists && is_valid_session "$session_file"; then echo "Resuming task session: $TASK_SESSION_ID" ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions) @@ -181,7 +173,7 @@ function start_agentapi() { ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions) [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi - + else if has_any_sessions; then echo "Continuing most recent standalone session" @@ -193,7 +185,7 @@ function start_agentapi() { [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) fi fi - + else echo "Continue disabled, starting fresh session" [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") From 04a160c24e9255848136fb8b98f01018dff21cde Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 14:59:41 -0600 Subject: [PATCH 06/20] fix(tests): update timestamps in session test cases to 2020 for consistency --- registry/coder/modules/claude-code/main.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 1ed1699f9..6e37501da 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -216,8 +216,8 @@ describe("claude-code", async () => { "bash", "-c", `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' -{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2025-12-03T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-03T10:00:05.000Z"} +{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} SESSIONEOF`, ]); @@ -397,8 +397,8 @@ SESSIONEOF`, "bash", "-c", `cat > ${sessionDir}/generic-123.jsonl << 'EOF' -{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2025-12-02T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T10:00:05.000Z"} +{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} EOF`, ]); @@ -436,8 +436,8 @@ EOF`, "bash", "-c", `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' -{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2025-12-02T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T10:00:05.000Z"} +{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} EOF`, ]); @@ -446,8 +446,8 @@ EOF`, "bash", "-c", `cat > ${sessionDir}/manual-456.jsonl << 'EOF' -{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2025-12-02T12:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2025-12-02T12:00:05.000Z"} +{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} EOF`, ]); From 3e5d65ab1248480ffcef181685ba58c2bb0da583 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 15:16:56 -0600 Subject: [PATCH 07/20] fix(claude-code): enhance session validation in start.sh to check for valid sessionId --- registry/coder/modules/claude-code/scripts/start.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 3c86477e6..45df168fe 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -115,7 +115,13 @@ is_valid_session() { local line_count=$(wc -l < "$session_file") if [ "$line_count" -lt 2 ]; then - printf "Session validation failed: incomplete (only %d lines)\n" "$line_count" + printf "Session validation failed: incomplete (only %s lines)\n" "$line_count" + return 1 + fi + + if ! grep -q '"sessionId"' "$session_file" || \ + ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' >/dev/null 2>&1; then + printf "Session validation failed: no valid sessionId found\n" return 1 fi From 5adfd099f9cd41630586e127899131517e2f59f0 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 15:21:28 -0600 Subject: [PATCH 08/20] chore: bun fmt --- registry/coder/modules/claude-code/scripts/start.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 45df168fe..a1bf63a85 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -119,8 +119,8 @@ is_valid_session() { return 1 fi - if ! grep -q '"sessionId"' "$session_file" || \ - ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' >/dev/null 2>&1; then + if ! grep -q '"sessionId"' "$session_file" \ + || ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then printf "Session validation failed: no valid sessionId found\n" return 1 fi From fdb9391535d12c2276e47513d6287fc24ef5c80e Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 3 Dec 2025 15:28:36 -0600 Subject: [PATCH 09/20] chore: shellcheck --- .../modules/claude-code/scripts/start.sh | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index a1bf63a85..5be29329c 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -87,9 +87,12 @@ function validate_claude_installation() { TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" task_session_exists() { - local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - local project_dir="$HOME/.claude/projects/${workdir_normalized}" - local session_file="$project_dir/${TASK_SESSION_ID}.jsonl" + local workdir_normalized + local project_dir + local session_file + workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + project_dir="$HOME/.claude/projects/${workdir_normalized}" + session_file="$project_dir/${TASK_SESSION_ID}.jsonl" if [ -f "$session_file" ]; then printf "Task session file found: %s\n" "$session_file" @@ -113,7 +116,8 @@ is_valid_session() { return 1 fi - local line_count=$(wc -l < "$session_file") + local line_count + line_count=$(wc -l < "$session_file") if [ "$line_count" -lt 2 ]; then printf "Session validation failed: incomplete (only %s lines)\n" "$line_count" return 1 @@ -130,8 +134,10 @@ is_valid_session() { } has_any_sessions() { - local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - local project_dir="$HOME/.claude/projects/${workdir_normalized}" + local workdir_normalized + local project_dir + workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + project_dir="$HOME/.claude/projects/${workdir_normalized}" if [ -d "$project_dir" ] && find "$project_dir" -name "*.jsonl" 2> /dev/null | grep -q .; then printf "Sessions found in: %s\n" "$project_dir" @@ -167,9 +173,12 @@ function start_agentapi() { elif [ "$ARG_CONTINUE" = "true" ]; then if [ "$ARG_REPORT_TASKS" = "true" ]; then - local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - local project_dir="$HOME/.claude/projects/${workdir_normalized}" - local session_file="$project_dir/${TASK_SESSION_ID}.jsonl" + local workdir_normalized + local project_dir + local session_file + workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + project_dir="$HOME/.claude/projects/${workdir_normalized}" + session_file="$project_dir/${TASK_SESSION_ID}.jsonl" if task_session_exists && is_valid_session "$session_file"; then echo "Resuming task session: $TASK_SESSION_ID" From 59fb5c024c0c674d5e98436685e13c1b421f0991 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 4 Dec 2025 15:52:05 -0600 Subject: [PATCH 10/20] fix(claude-code): enhance session validation in start.sh to remove stale, empty, or malformed session files --- .../modules/claude-code/scripts/start.sh | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 5be29329c..4d631672a 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -106,26 +106,43 @@ task_session_exists() { is_valid_session() { local session_file="$1" - if [ ! -f "$session_file" ] || [ ! -s "$session_file" ]; then - printf "Session validation failed: file missing or empty\n" + # Check if file exists and is not empty + # Empty files indicate the session was created but never used so they need to be removed + if [ ! -f "$session_file" ]; then + printf "Session validation failed: file does not exist\n" return 1 fi + if [ ! -s "$session_file" ]; then + printf "Session validation failed: file is empty, removing stale file\n" + rm -f "$session_file" + return 1 + fi + + # Validate JSONL format by checking first 3 lines + # Claude session files use JSONL (JSON Lines) format where each line is valid JSON if ! head -3 "$session_file" | jq empty 2> /dev/null; then - printf "Session validation failed: invalid JSONL format\n" + printf "Session validation failed: invalid JSONL format, removing corrupt file\n" + rm -f "$session_file" return 1 fi + # Check for minimum session content + # Valid sessions need at least 2 lines: initial message and first response local line_count line_count=$(wc -l < "$session_file") if [ "$line_count" -lt 2 ]; then - printf "Session validation failed: incomplete (only %s lines)\n" "$line_count" + printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count" + rm -f "$session_file" return 1 fi + # Verify the session has a valid sessionId field + # This ensures the file structure matches Claude's session format if ! grep -q '"sessionId"' "$session_file" \ || ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then - printf "Session validation failed: no valid sessionId found\n" + printf "Session validation failed: no valid sessionId found, removing malformed file\n" + rm -f "$session_file" return 1 fi From bf22ad63bbb011e6abbd2525db5fa36ed7fa14fc Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 5 Dec 2025 09:14:36 -0600 Subject: [PATCH 11/20] fix(claude-code): ensure AI prompt argument is consistently added in start.sh for fresh sessions --- registry/coder/modules/claude-code/scripts/start.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 4d631672a..221c4101f 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -213,15 +213,15 @@ function start_agentapi() { [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) else echo "No sessions found, starting fresh standalone session" - [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) + [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi fi else echo "Continue disabled, starting fresh session" - [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) + [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" From 247874cfef543535f8649d1e4b331465250f13cd Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 5 Dec 2025 09:47:55 -0600 Subject: [PATCH 12/20] fix(claude-code): update environment variable name from CLAUDE_API_KEY to ANTHROPIC_API_KEY --- registry/coder/modules/claude-code/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index db24493b5..45784a201 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -276,7 +276,7 @@ resource "coder_env" "claude_api_key" { count = length(var.claude_api_key) > 0 ? 1 : 0 agent_id = var.agent_id - name = "CLAUDE_API_KEY" + name = "ANTHROPIC_API_KEY" value = var.claude_api_key } From 0e52e070f9a1de900cc26f01c629b1266a16668e Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 5 Dec 2025 10:23:23 -0600 Subject: [PATCH 13/20] fix(claude-code): rename environment variable to CLAUDE_API_KEY and add standalone mode configuration --- registry/coder/modules/claude-code/main.tf | 2 +- .../modules/claude-code/scripts/install.sh | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 45784a201..db24493b5 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -276,7 +276,7 @@ resource "coder_env" "claude_api_key" { count = length(var.claude_api_key) > 0 ? 1 : 0 agent_id = var.agent_id - name = "ANTHROPIC_API_KEY" + name = "CLAUDE_API_KEY" value = var.claude_api_key } diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 8aa0d1ac8..61c634319 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -90,12 +90,63 @@ function setup_claude_configurations() { } +function configure_standalone_mode() { + echo "Configuring Claude Code for standalone mode..." + + if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "Note: ANTHROPIC_API_KEY not set, skipping authentication setup" + return + fi + + local claude_config="$HOME/.claude.json" + local workdir_normalized + workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + + # Create or update .claude.json with minimal configuration for API key auth + # This skips the interactive login prompt and onboarding screens + if [ -f "$claude_config" ]; then + echo "Updating existing Claude configuration at $claude_config" + + jq --arg apikey "$ANTHROPIC_API_KEY" \ + --arg workdir "$ARG_WORKDIR" \ + '.autoUpdaterStatus = "disabled" | + .bypassPermissionsModeAccepted = true | + .hasAcknowledgedCostThreshold = true | + .hasCompletedOnboarding = true | + .primaryApiKey = $apikey | + .projects[$workdir].hasCompletedProjectOnboarding = true | + .projects[$workdir].hasTrustDialogAccepted = true' \ + "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" + else + echo "Creating new Claude configuration at $claude_config" + cat > "$claude_config" << EOF +{ + "autoUpdaterStatus": "disabled", + "bypassPermissionsModeAccepted": true, + "hasAcknowledgedCostThreshold": true, + "hasCompletedOnboarding": true, + "primaryApiKey": "$ANTHROPIC_API_KEY", + "projects": { + "$ARG_WORKDIR": { + "hasCompletedProjectOnboarding": true, + "hasTrustDialogAccepted": true + } + } +} +EOF + fi + + echo "Standalone mode configured successfully" +} + function report_tasks() { if [ "$ARG_REPORT_TASKS" = "true" ]; then echo "Configuring Claude Code to report tasks via Coder MCP..." export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG" export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" coder exp mcp configure claude-code "$ARG_WORKDIR" + else + configure_standalone_mode fi } From 0a6660d5e61ff82722884f3af16430ffc50bfe7d Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 5 Dec 2025 10:33:35 -0600 Subject: [PATCH 14/20] fix(claude-code): update environment variable references to CLAUDE_API_KEY in standalone mode configuration --- registry/coder/modules/claude-code/scripts/install.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 61c634319..85849ff1a 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -93,8 +93,8 @@ function setup_claude_configurations() { function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." - if [ -z "$ANTHROPIC_API_KEY" ]; then - echo "Note: ANTHROPIC_API_KEY not set, skipping authentication setup" + if [ -z "${CLAUDE_API_KEY:-}" ]; then + echo "Note: CLAUDE_API_KEY not set, skipping authentication setup" return fi @@ -106,8 +106,8 @@ function configure_standalone_mode() { # This skips the interactive login prompt and onboarding screens if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" - - jq --arg apikey "$ANTHROPIC_API_KEY" \ + + jq --arg apikey "${CLAUDE_API_KEY:-}" \ --arg workdir "$ARG_WORKDIR" \ '.autoUpdaterStatus = "disabled" | .bypassPermissionsModeAccepted = true | @@ -125,7 +125,7 @@ function configure_standalone_mode() { "bypassPermissionsModeAccepted": true, "hasAcknowledgedCostThreshold": true, "hasCompletedOnboarding": true, - "primaryApiKey": "$ANTHROPIC_API_KEY", + "primaryApiKey": "${CLAUDE_API_KEY:-}", "projects": { "$ARG_WORKDIR": { "hasCompletedProjectOnboarding": true, From 3903a081956132b9146aa69f7b80627c52739ad1 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 5 Dec 2025 11:45:45 -0600 Subject: [PATCH 15/20] chore: bun fmt --- .../coder/modules/claude-code/scripts/install.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 85849ff1a..ba4420fa3 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -92,31 +92,31 @@ function setup_claude_configurations() { function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." - + if [ -z "${CLAUDE_API_KEY:-}" ]; then echo "Note: CLAUDE_API_KEY not set, skipping authentication setup" return fi - + local claude_config="$HOME/.claude.json" local workdir_normalized workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - + # Create or update .claude.json with minimal configuration for API key auth # This skips the interactive login prompt and onboarding screens if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" - + jq --arg apikey "${CLAUDE_API_KEY:-}" \ - --arg workdir "$ARG_WORKDIR" \ - '.autoUpdaterStatus = "disabled" | + --arg workdir "$ARG_WORKDIR" \ + '.autoUpdaterStatus = "disabled" | .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | .hasCompletedOnboarding = true | .primaryApiKey = $apikey | .projects[$workdir].hasCompletedProjectOnboarding = true | .projects[$workdir].hasTrustDialogAccepted = true' \ - "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" + "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" else echo "Creating new Claude configuration at $claude_config" cat > "$claude_config" << EOF @@ -135,7 +135,7 @@ function configure_standalone_mode() { } EOF fi - + echo "Standalone mode configured successfully" } From 697a66cff1bc159ac64ea35eeadee1b3a10202e3 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 8 Dec 2025 15:34:20 -0600 Subject: [PATCH 16/20] fix: limit search depth for JSONL files in session validation --- registry/coder/modules/claude-code/scripts/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 221c4101f..6ff9eeff7 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -156,7 +156,7 @@ has_any_sessions() { workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') project_dir="$HOME/.claude/projects/${workdir_normalized}" - if [ -d "$project_dir" ] && find "$project_dir" -name "*.jsonl" 2> /dev/null | grep -q .; then + if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" 2> /dev/null | grep -q .; then printf "Sessions found in: %s\n" "$project_dir" return 0 else From 5ff8af1ee88a6ad9e826fd102a91fb1c9c730b03 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 8 Dec 2025 15:42:34 -0600 Subject: [PATCH 17/20] refactor: reorder JSONL format validation in session validation logic --- .../coder/modules/claude-code/scripts/start.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 6ff9eeff7..2b2b797e9 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -119,14 +119,6 @@ is_valid_session() { return 1 fi - # Validate JSONL format by checking first 3 lines - # Claude session files use JSONL (JSON Lines) format where each line is valid JSON - if ! head -3 "$session_file" | jq empty 2> /dev/null; then - printf "Session validation failed: invalid JSONL format, removing corrupt file\n" - rm -f "$session_file" - return 1 - fi - # Check for minimum session content # Valid sessions need at least 2 lines: initial message and first response local line_count @@ -137,6 +129,14 @@ is_valid_session() { return 1 fi + # Validate JSONL format by checking first 3 lines + # Claude session files use JSONL (JSON Lines) format where each line is valid JSON + if ! head -3 "$session_file" | jq empty 2> /dev/null; then + printf "Session validation failed: invalid JSONL format, removing corrupt file\n" + rm -f "$session_file" + return 1 + fi + # Verify the session has a valid sessionId field # This ensures the file structure matches Claude's session format if ! grep -q '"sessionId"' "$session_file" \ From 404cc694f88b06691baf32c441600d500a022cd2 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 8 Dec 2025 15:55:31 -0600 Subject: [PATCH 18/20] fix: ensure JSONL files are non-empty in session validation --- registry/coder/modules/claude-code/scripts/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 2b2b797e9..4a1d304b7 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -156,7 +156,7 @@ has_any_sessions() { workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') project_dir="$HOME/.claude/projects/${workdir_normalized}" - if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" 2> /dev/null | grep -q .; then + if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then printf "Sessions found in: %s\n" "$project_dir" return 0 else From 0aa2847a25312917e00a817b44051ea72ce85d76 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 8 Dec 2025 16:08:46 -0600 Subject: [PATCH 19/20] refactor: extract project directory and session file retrieval into separate functions --- .../modules/claude-code/scripts/start.sh | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 4a1d304b7..93ff4f723 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -86,13 +86,19 @@ function validate_claude_installation() { # This ensures all task sessions use a consistent, predictable ID TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" -task_session_exists() { +get_project_dir() { local workdir_normalized - local project_dir - local session_file workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - project_dir="$HOME/.claude/projects/${workdir_normalized}" - session_file="$project_dir/${TASK_SESSION_ID}.jsonl" + echo "$HOME/.claude/projects/${workdir_normalized}" +} + +get_task_session_file() { + echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl" +} + +task_session_exists() { + local session_file + session_file=$(get_task_session_file) if [ -f "$session_file" ]; then printf "Task session file found: %s\n" "$session_file" @@ -151,10 +157,8 @@ is_valid_session() { } has_any_sessions() { - local workdir_normalized local project_dir - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - project_dir="$HOME/.claude/projects/${workdir_normalized}" + project_dir=$(get_project_dir) if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then printf "Sessions found in: %s\n" "$project_dir" @@ -190,12 +194,8 @@ function start_agentapi() { elif [ "$ARG_CONTINUE" = "true" ]; then if [ "$ARG_REPORT_TASKS" = "true" ]; then - local workdir_normalized - local project_dir local session_file - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - project_dir="$HOME/.claude/projects/${workdir_normalized}" - session_file="$project_dir/${TASK_SESSION_ID}.jsonl" + session_file=$(get_task_session_file) if task_session_exists && is_valid_session "$session_file"; then echo "Resuming task session: $TASK_SESSION_ID" From 06b031f5be576e97cac069b3afab55fa85dfeb7e Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 9 Dec 2025 16:00:15 +0500 Subject: [PATCH 20/20] Update agent_id references in README.md --- registry/coder/modules/claude-code/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index a95b8dc30..6376c3716 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -229,31 +229,31 @@ variable "vertex_sa_json" { } resource "coder_env" "vertex_use" { - agent_id = coder_agent.example.id + agent_id = coder_agent.main.id name = "CLAUDE_CODE_USE_VERTEX" value = "1" } resource "coder_env" "vertex_project_id" { - agent_id = coder_agent.example.id + agent_id = coder_agent.main.id name = "ANTHROPIC_VERTEX_PROJECT_ID" value = "your-gcp-project-id" } resource "coder_env" "cloud_ml_region" { - agent_id = coder_agent.example.id + agent_id = coder_agent.main.id name = "CLOUD_ML_REGION" value = "global" } resource "coder_env" "vertex_sa_json" { - agent_id = coder_agent.example.id + agent_id = coder_agent.main.id name = "VERTEX_SA_JSON" value = var.vertex_sa_json } resource "coder_env" "google_application_credentials" { - agent_id = coder_agent.example.id + agent_id = coder_agent.main.id name = "GOOGLE_APPLICATION_CREDENTIALS" value = "/tmp/gcp-sa.json" }