מדריך קלוד קוד בעברית

תיעוד 105

בדיקת סביבות באירוח עצמי מקצה לקצה

אימות של image של runner באירוח עצמי מתוך CI: שיגור session באמצעות ה-CLI, קריאת התשובות של Claude דרך Stop hook, ותסרוט של הלולאה המלאה בקוד.

הערה: סביבות באירוח עצמי נמצאות בבטא ציבורית (public beta) בתוכניות Team ו-Enterprise. הסעיף זמינות ומגבלות מתאר את תהליך ההפעלה. דף זה מציג את מתכון הבדיקה ב-CI. למידע על הגדרה ראשונית עיינו ב-מדריך ההתחלה המהירה, ועיינו ב-פריסה לסביבת ייצור למתכוני פריסה של צי שרתים (fleet).

ב-סביבה באירוח עצמי, הפעלות ענן (cloud sessions) של Claude Code רצות על גבי image של runner שאתם בונים ומתחזקים. לפני שמפיצים image חדש לסביבת הייצור שלכם, מריצים session מלא מול סביבת בדיקה באמצעות סקריפט: יוצרים session, קוראים את התשובה של Claude, שולחים הודעת המשך, וקוראים גם את התשובה לה. זהו המבנה של smoke test ב-CI שמאמת את ה-image של ה-runner, את הגישה ל-git, ואת כל הכלים המותאמים אישית לפני שמקדמים שינוי.

מתכון זה מניח שכבר הגדרתם סביבה ו-runner, ושמשימת ה-CI שלכם מפעילה את תהליך ה-runner על אותו מארח (host) שבו רץ סקריפט הבדיקה, שזהו המבנה הטבעי לבדיקת image חדש של runner. כלי מסוג Stop hook שאתם מתקינים ב-runner כותב את התשובה הסופית של כל תור (turn) לקובץ מקומי, והסקריפט קורא אותה משם, כך שהפניות היחידות ל-API של Anthropic הן שני השיגורים עצמם. אם ה-runners של הבדיקה שלכם נמצאים בתשתית נפרדת, ראו runners מרוחקים לבדיקה.

#התקנת ה-capture hook ב-runner הבדיקה שלכם

קריאת התשובות בחזרה (read-back) מתבצעת באמצעות Stop hook של Claude Code: כאשר Claude מסיים תור (turn), ה-hook מקבל את הודעת ה-assistant הסופית כערך last_assistant_message ב-JSON שמגיע ל-stdin שלו, ומוסיף אותה לסוף הקובץ $E2E_REPLY_DIR/<session_id>.txt. התקינו אותו באותו אופן כמו ה-commit-nudge Stop hook, בנתיב ~/.claude/ במארח של ה-runner, שאותו ה-runner משכפל לכל session.

#שמירת קובצי ה-hook

שמרו את שני הקבצים הבאים במארח של ה-runner:

  • בלוק ההגדרות: מזגו לתוך ~/.claude/settings.json במארח של ה-runner
  • הסקריפט: שמרו כ-~/.claude/hooks/e2e-stop-hook-capture.sh במארח של ה-runner והפכו אותו לבר-ביצוע (executable)
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "timeout": 10,
            "command": "\"$CLAUDE_CONFIG_DIR/hooks/e2e-stop-hook-capture.sh\""
          }
        ]
      }
    ]
  }
}
#!/bin/sh
# Stop hook for testing a self-hosted environment end to end: writes each
# turn's final assistant reply to $E2E_REPLY_DIR/<session_id>.txt so a
# co-located test driver can read it without calling the Anthropic API.
# Install on the TEST runner only. Requires jq.

# No-op unless the driver is listening. Never fail the turn.
[ -n "${E2E_REPLY_DIR:-}" ] && [ -d "$E2E_REPLY_DIR" ] || exit 0

# CLAUDE_CODE_REMOTE_SESSION_ID is exported in cse_... form; the session
# id the dispatch CLI prints is in session_... form. Same id, different
# prefix.
sid=$(printf '%s' "${CLAUDE_CODE_REMOTE_SESSION_ID:-}" | sed 's/^cse_/session_/')
[ -n "$sid" ] || exit 0

# last_assistant_message is absent when the final assistant turn had no
# text, such as a tool-use-only turn. The `// empty` filter makes that a
# zero-byte write rather than the literal string "null".
jq -r '.last_assistant_message // empty' >> "$E2E_REPLY_DIR/$sid.txt" 2>/dev/null
exit 0

#לפני הפעלת ה-runner

שני דברים שה-hook תלוי בהם:

  • התקינו אותו לפני הפעלת ה-runner. ה-runner לוקח snapshot של ~/.claude/ פעם אחת בעת ההפעלה, לכן hook שנוסף ל-runner שכבר רץ ייכנס לתוקף רק לאחר הפעלה מחדש.
  • ייצאו את משתנה הסביבה E2E_REPLY_DIR לתהליך ה-runner. ה-hook אינו מבצע דבר כאשר המשתנה אינו מוגדר או כאשר התיקייה אינה קיימת, לכן הגדירו אותו בכל מקום שבו אתם מפעילים את ה-runner, כגון ב-systemd unit, ב-pod spec, או בשלב ב-CI. סקריפט הבדיקה שלהלן דורש אותו גם כן.

התקינו hook זה רק ב-runners שמשרתים את סביבת הבדיקה שלכם. הוא כותב לדיסק את התשובה הסופית של כל session בכל פעם ש-E2E_REPLY_DIR קיים. התנהגות זו אינה מזיקה ב-runner ארעי של CI, אך אינה משהו שכדאי לכלול ב-image של runner בסביבת ייצור, שבה המשתנה עלול להיות מוגדר בטעות.

#הרצת לולאת הבדיקה

הדגלים --environment ו---ref של פקודת השיגור דורשים גרסת Claude Code v2.1.224 ומעלה במכונה שמריצה את הסקריפט, שזהו אותו רף גרסה מינימלי הנדרש עבור ה-runner עצמו. כאשר ה-hook מותקן וה-runner מופעל במארח זה, סקריפט הבדיקה מבצע את השלבים הבאים:

  1. יוצר session בסביבת הבדיקה באמצעות claude -p "<prompt>" --environment <environment-id> --output-format json, בהרצה מתוך תיקיית git checkout כדי שה-CLI יוכל לזהות אוטומטית את המאגר (repository) מתוך ה-remote ששמו origin. הדגל האופציונלי --ref <branch> מבסס את ה-checkout של ה-session על ref ספציפי במקום על ה-HEAD המקומי. הפקודה יוצרת את ה-session, מדפיסה שורת JSON יחידה המכילה את session_id, ומסתיימת בלי להמתין לתשובה של Claude.
  2. ממתין שהתשובה תופיע בקובץ $E2E_REPLY_DIR/<session_id>.txt, שנכתב על ידי ה-Stop hook ב-runner ברגע שהתור (turn) מסתיים.
  3. שולח הודעת המשך באמצעות claude -p "<message>" --cloud <session_id> --output-format json (ראו שליחת הודעת המשך ל-session פעיל), פקודה שמפרסמת אירוע משתמש (user event) ל-session הקיים ומסתיימת.
  4. ממתין לתשובה להודעת ההמשך באותו אופן כמו בשלב 2.

#התנהגות שיגור עם --environment

Claude Code יוצר את ה-session, מדפיס את מזהה ה-session וקישור אליו, ומסתיים.

הדגל מקבל עדיפות על פני ההגדרה remote.defaultEnvironmentId. הוא אינו תומך ב---output-format stream-json, ואי אפשר לשלב אותו עם דגלים שמחדשים, מתחברים או מגדירים מראש session, כגון --resume, --continue, --teleport, --session-id, או --init-only. הדגל --cloud נדחה כאשר מועבר עימו מזהה session או כתובת URL, וכן בהרצות שאינן אינטראקטיביות כאשר הוא נושא תיאור. שימוש בדגל --cloud לבדו ללא ערכים נוספים נחשב כלא קיים. מתוך מסוף (terminal), ניתן להעביר את המשימה כתיאור של --cloud במקום כ-prompt מיקומי.

#סקריפט לדוגמה

הסקריפט שלהלן מריץ את הלולאה המלאה מול $CLAUDE_TEST_ENVIRONMENT_ID, מזהה ה-ccpool_... של סביבת הבדיקה שלכם, המוצג בתיבת פרטי הסביבה בדף הניהול (admin page) או מוחזר על ידי הקריאה ליצירת סביבה, ומבצע בדיקת נכונות (assertion) על משפט ביקורת (sentinel phrase) בכל תשובה. הריצו אותו מתוך עותק checkout של git במאגר שבו אתם רוצים שה-session יעבוד, לאחר הפעלת runner במארח זה כאשר ה-capture hook מותקן והמשתנה E2E_REPLY_DIR מיוצא.

#!/usr/bin/env bash
# End-to-end test against a self-hosted environment, using Stop-hook read-back.
# Prereqs: `claude auth login` has been run on this machine (see "Authenticate
# from CI" below); jq is installed; CLAUDE_TEST_ENVIRONMENT_ID names an
# environment whose runner is the one on this host, with the capture hook
# installed and E2E_REPLY_DIR in its environment.

set -euo pipefail

: "${CLAUDE_TEST_ENVIRONMENT_ID:=${CLAUDE_TEST_POOL_ID:-}}"  
# CLAUDE_TEST_POOL_ID is the legacy spelling
: "${CLAUDE_TEST_ENVIRONMENT_ID:?set CLAUDE_TEST_ENVIRONMENT_ID to a ccpool_... id served by a runner on this host}"
: "${E2E_REPLY_DIR:?set E2E_REPLY_DIR to the directory the Stop hook on your test runner writes to, and export it to the runner process}"
: "${TEST_REPO_REF:=main}"

[ -d "$E2E_REPLY_DIR" ] || {
  echo "FAIL: E2E_REPLY_DIR ($E2E_REPLY_DIR) does not exist. The Stop hook on the runner needs it." >&2
  exit 1
}

# Waits until $E2E_REPLY_DIR/<session_id>.txt contains $2, or fails after
# 90 seconds. Tune the timeout to your environment's cold-start time. The
# file is written by the Stop hook on the runner.
await_reply() {
  local expect="$2" f="$E2E_REPLY_DIR/$1.txt"
  local deadline=$(($(date +%s) + 90))
  while :; do
    if [ -f "$f" ] && grep -qF -- "$expect" "$f"; then
      return
    fi
    [ "$(date +%s)" -lt "$deadline" ] || {
      echo "FAIL: '$expect' not in $f within 90s. The Stop hook on the runner did not write it." >&2
      echo "-- $E2E_REPLY_DIR contents --" >&2; ls -la "$E2E_REPLY_DIR" >&2
      [ -f "$f" ] && { echo "-- $f --" >&2; cat "$f" >&2; }
      exit 1
    }
    sleep 1
  done
}

# 1. Create the session on the test environment. Run from a git checkout
# so the CLI can auto-detect the repo. --ref pins the checkout to a named
# ref regardless of local HEAD.
TURN1="e2e-probe-$(date +%s)-$$: say exactly 'ok: custom tools are reachable' and nothing else"
EXPECT1="ok: custom tools are reachable"
create_json=$(claude -p "$TURN1" --environment "$CLAUDE_TEST_ENVIRONMENT_ID" \
  --ref "$TEST_REPO_REF" --output-format json)
echo "create: $create_json"
SESSION_ID=$(jq -er '.session_id' <<<"$create_json")

# 2. Wait for the turn-1 reply.
await_reply "$SESSION_ID" "$EXPECT1"
echo "turn-1 reply ok"

# 3. Post a follow-up via the CLI.
TURN2="e2e-probe-followup-$(date +%s): say exactly 'ok: follow-up delivered' and nothing else"
EXPECT2="ok: follow-up delivered"
followup_json=$(claude -p "$TURN2" --cloud "$SESSION_ID" --output-format json)
echo "followup: $followup_json"
jq -e '.ok == true' <<<"$followup_json" >/dev/null

# 4. Wait for the turn-2 reply.
await_reply "$SESSION_ID" "$EXPECT2"
echo "turn-2 reply ok"

echo "PASS: test-environment round-trip (session $SESSION_ID)"

החליפו את ה-prompts של TURN1 ו-TURN2 ואת ביטויי הביקורת EXPECT1 ו-EXPECT2 בכל מה שבודק את ההגדרה שלכם, כגון בקשה מ-Claude להפעיל אחד מתוך כלי ה-MCP המותאמים אישית שלכם ובדיקת הפלט שלו.

#runners מרוחקים לבדיקה

אם ה-runners של הבדיקה שלכם נמצאים בתשתית נפרדת, כגון צי Kubernetes קבוע שמשימת ה-CI שלכם אינה יכולה לחלוק עימו מערכת קבצים, החליפו את פעולת כתיבת הקובץ ב-Stop hook בפעולת POST ל-endpoint שה-driver שלכם מאזין לו:

#!/bin/sh
# Variant of the capture hook for runners on separate infrastructure.
# Set E2E_REPLY_URL on the runner to an endpoint the driver controls.
[ -n "${E2E_REPLY_URL:-}" ] || exit 0
sid=$(printf '%s' "${CLAUDE_CODE_REMOTE_SESSION_ID:-}" | sed 's/^cse_/session_/')
[ -n "$sid" ] || exit 0
jq -r '.last_assistant_message // empty' | \
  curl -fsS -X POST --data-binary @- "$E2E_REPLY_URL/$sid" >/dev/null 2>&1
exit 0

בצד ה-driver, הפעילו כל רכיב שיכול לקבל את ה-POST ולהחזיק את התשובה עד שהבדיקה תבקש אותה, כגון מאזין HTTP קטן בתוך משימת ה-CI או מקבל webhook שכבר פועל אצלכם. ה-hook רץ בתשתית שלכם, כך שכל מה שנדרש הוא שה-endpoint יהיה נגיש מה-runners שלכם.

#אימות מתוך CI

הפקודות claude -p ... --environment ו-claude -p ... --cloud מבצעות שתיהן אימות באמצעות token של claude.ai OAuth. מפתחות API, כגון sk-ant-xxxxx, אינם מתקבלים באף אחת משתי הקריאות. שתי גישות מאפשרות להפוך token לזמין ב-CI.

#מארח CI ארוך טווח

הריצו claude auth login פעם אחת באופן אינטראקטיבי במכונה שמריצה את הסקריפט, תוך שימוש בחשבון משתמש ייעודי לאוטומציה. Claude Code שומר את ה-token ב-keychain של מערכת ההפעלה ב-macOS, או ב-~/.claude/.credentials.json ב-Linux וב-Windows. במארח macOS שבו לא ניתן לכתוב ל-Keychain, כפי שקורה בדרך כלל בחיבור SSH שבו ה-login Keychain נשאר נעול, Claude Code שומר את ה-token ב-~/.claude/.credentials.json גם שם. ראו ניהול פרטי גישה.

ה-CLI מרענן את ה-access token קצר המועד באופן אוטומטי בכל הפעלה, אך הרשאת ה-refresh-token הבסיסית מוגבלת ל-30 יום ממועד ההתחברות הראשונית, לכן יש להריץ שוב claude auth login באופן אינטראקטיבי באותו מארח מדי 30 יום.

#runners ארעיים ב-CI

כיום אין token ארוך טווח ל-CI עבור תרחיש זה. ה-scope שמעניק שליטה ב-sessions מרוחקים, user:sessions:claude_code, מוגבל בצד השרת ל-30 יום, ולכן הפקודה claude setup-token, שמנפיקה token לשנה אחת המיועד להסקת מודל בלבד (inference-only), אינה מכסה זאת. ה-סוד של הסביבה (environment secret) אינו מתקבל אף הוא, מכיוון שהוא מאשר ל-runner רק להירשם לסביבה, ולא ליצור sessions.

כדי להגדיר התחברות שמורה מראש ב-runner ארעי, הגדירו את CLAUDE_CODE_OAUTH_REFRESH_TOKEN ו-CLAUDE_CODE_OAUTH_SCOPES כך ש-claude auth login יחליף את ה-token ללא דפדפן. אותה מגבלה של 30 יום חלה על הרשאת הריענון (refresh grant). צרו קשר עם צוות הלקוחות שלכם ב-Anthropic אם אתם זקוקים למסלול זיהוי מכונה (machine-identity) שאינו קשור לחשבון של אדם.

#יצירת סביבת בדיקה ייעודית

צרו ומחקו סביבות באופן פרוגרמטי כדי שכל הרצת CI תקבל סביבה נקייה. ה-runner שמשימת ה-CI שלכם מפעילה נרשם לתוך הסביבה החדשה. קריאות היצירה והמחיקה שלהלן הן אותם endpoints שבהם משתמש דף הניהול Cloud environments ב-claude.ai, והן דורשות את הכותר (header) הבא: anthropic-beta: ccr-byoc-2025-07-29.

#הנפקת ה-admin token

המשתנה $ADMIN_TOKEN הוא access token של claude.ai OAuth עבור חשבון בעל תפקיד Owner, המונפק באותו אופן המתואר בסעיף אימות מתוך CI:

  • הנפיקו אותו: הריצו claude auth login עם חשבון בעל תפקיד Owner, ולאחר מכן קראו את ה-access token הנוכחי מהמיקום שבו Claude Code שמר אותו, כפי שמוסבר בסעיף מארח CI ארוך טווח.
  • קראו אותו מחדש בכל הרצה: ה-CLI מבצע רוטציה ל-access token, ואותה הגבלה של 30 יום להרשאת הריענון חלה גם כאן, לכן אל תשמרו עותק שלו.
  • העבירו אותו דרך stdin: כפי שנעשה בדוגמה, כך שה-token לעולם לא יופיע ברשימת הארגומנטים של curl או ביומן הבנייה (build log) שלכם.

#יצירת הסביבה

קלטו את התגובה מבלי להדפיס אותה בחזרה (echo): pool_secret הוא פרט גישה ארוך טווח המאפשר לרשום runners לתוך הסביבה, לכן שמרו אותו כ-masked secret ב-CI והדפיסו רק את ה-environment ID. התחביר -H @- ששומר על ה-token מחוץ לרשימת התהליכים דורש curl 7.55 ומעלה. גרסאות ישנות יותר של curl מתייחסות ל-@- כאל כותר מילולי ושולחות את הבקשה ללא אימות.

create=$(curl -fsS -X POST -H @- \
  -H "anthropic-beta: ccr-byoc-2025-07-29" -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"name":"ci-test-environment"}' \
  https://api.anthropic.com/v1/code/runners/self-hosted/pools \
  <<<"Authorization: Bearer $ADMIN_TOKEN")
ENVIRONMENT_ID=$(jq -er .pool.pool_id <<<"$create")
ENVIRONMENT_SECRET=$(jq -er .pool_secret <<<"$create")

עד שמשתמש בעל תפקיד Owner מפעיל את Allow self-hosted environments עבור הארגון, הקריאה תיכשל עם שגיאת 403 מסוג permission_error עם ההודעה self-hosted runners are disabled by your organization's policy.

הפעילו runner במארח זה עם SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET=$ENVIRONMENT_SECRET, יחד עם ה-capture hook ו-E2E_REPLY_DIR בהתאם לסעיף התקנת ה-capture hook, ולאחר מכן הריצו את סקריפט הבדיקה.

#מחיקת הסביבה

מחקו את הסביבה כאשר ההרצה מסתיימת, כדי שכל הרצת CI תתחיל נקייה:

curl -fsS -X DELETE -H @- \
  -H "anthropic-beta: ccr-byoc-2025-07-29" -H "anthropic-version: 2023-06-01" \
  "https://api.anthropic.com/v1/code/runners/self-hosted/pools/$ENVIRONMENT_ID" \
  <<<"Authorization: Bearer $ADMIN_TOKEN"