Claude Agent SDK: własni agenci na pętli Claude Code
Claude Agent SDK to biblioteka dla Pythona i TypeScriptu, która uruchamia binarkę Claude Code jako podproces i udostępnia kodowi aplikacji jej pętlę agenta: wbudowane narzędzia, reguły uprawnień, hooki, subagentów, serwery MCP i wznawialne sesje. Pasuje do botów, które potrzebują własnych narzędzi w procesie, zabezpieczeń w kodzie albo typowanych wyników, których jedno wywołanie claude -p nie wyrazi.
CI w twoim zespole robi się czerwone dwadzieścia razy w tygodniu i za każdym razem ktoś otwiera log na 4000 linii, przewija kaskadę błędów pochodnych i decyduje, czy to prawdziwa regresja, niestabilny test, czy zepsuty runner. Krok z claude -p już był: dał prozę, z którą nikt nic nie zrobił, raz powołał się na plik, którego nie ma, a raz poprosił o uprawnienie, którego nie miał kto zatwierdzić. Ta strona buduje wersję, która tego nie robi.
Co zbudujesz z Claude Agent SDK
Dział zatytułowany „Co zbudujesz z Claude Agent SDK”- Bota do triage’u CI w około 130 liniach Pythona (z odpowiednikiem w TypeScripcie), który klasyfikuje nieudany przebieg GitHub Actions i cytuje swoje dowody
- Konfigurację uprawnień, w której bot jest tylko do odczytu z konstrukcji, a nie z polecenia: bez
Bash, bezEdit, z odczytami ograniczonymi do checkoutu - Jedno własne narzędzie w procesie, jeden hook
PreToolUse, jednego subagenta i kontrakt w JSON Schema, każde z nazwanym zadaniem - Deterministyczną kontrolę, która odrzuca werdykt, jeśli jego dowodów nie ma w logu ani w repozytorium, oraz pętlę ewaluacyjną mierzącą bota na oznaczonych historycznych awariach
- Regułę decyzyjną: kiedy SDK się opłaca, a kiedy wystarczy
claude -palbo Routines (rutyny w chmurze)
Kiedy użyć Agent SDK zamiast claude -p?
Dział zatytułowany „Kiedy użyć Agent SDK zamiast claude -p?”Zacznij od najmniejszej rzeczy, która działa. Większość botów w CI nigdy nie potrzebuje SDK, a SDK wnosi do pipeline’u zależność, podproces i klucz API.
| Potrzebujesz | Użyj | Dlaczego |
|---|---|---|
| Jeden prompt, jedna odpowiedź JSON, w kroku powłoki | claude -p z --output-format json, --json-schema i --max-budget-usd | Zero kodu do utrzymania. Zobacz automatyzację zadań w trybie headless |
Przegląd PR albo odpowiadanie na @claude na GitHubie | anthropics/claude-code-action@v1 | Akcja sama obsługuje checkout, komentarze i tokeny. Zobacz CI/CD z Claude Code |
| Przebieg z harmonogramu albo webhooka w chmurze Anthropic | Routines | Żadnego runnera do utrzymania |
| Własne narzędzia w twoim języku, zabezpieczenia w kodzie, sterowanie wieloma turami, typowane wyniki, strumieniowanie do własnego UI | Agent SDK | Wszystko poniżej tej tabeli |
| Agent hostowany przez Anthropic, bez procesu, który musisz uruchamiać | Claude Managed Agents | Anthropic uruchamia pętlę i sandbox; płacisz za tokeny modelu plus 0,08 USD za godzinę sesji |
| Claude API z własną pętlą narzędzi, bez narzędzi Claude Code | Kliencki SDK Claude API | Agent SDK to Claude Code jako biblioteka; kliencki SDK to surowe API |
SDK istnieje tylko dla Pythona i TypeScriptu. Z każdego innego języka przegląd Agent SDK każe uruchamiać CLI jako podproces z -p i --output-format json. To samo zadanie napisane równolegle w Codex SDK i Cursor SDK znajdziesz w porównaniu trzech SDK do sterowania agentami z kodu.
Zainstaluj Agent SDK i skonfiguruj uwierzytelnianie
Dział zatytułowany „Zainstaluj Agent SDK i skonfiguruj uwierzytelnianie”# Python 3.10+. Wheel zawiera CLI Claude Code; nie trzeba nic więcej instalować.pip install claude-agent-sdk==0.2.160Żeby użyć systemowego claude zamiast dołączonego, przekaż cli_path="/path/to/claude" w ClaudeAgentOptions.
# Node 18+. zod to zależność peer, potrzebna do tool().npm install @anthropic-ai/claude-agent-sdk@0.3.283 zodBinarka Claude Code przychodzi jako opcjonalna zależność dla konkretnej platformy. Jeśli CI instaluje paczki z --omit=optional, binarki nie ma; doinstaluj ją i przekaż pathToClaudeCodeExecutable.
Uwierzytelniasz się kluczem API z Claude Console, wyeksportowanym jako ANTHROPIC_API_KEY w procesie, który uruchamia agenta. SDK nie wczytuje za ciebie plików .env. U dostawców chmurowych ustaw CLAUDE_CODE_USE_BEDROCK=1, CLAUDE_CODE_USE_VERTEX=1 (Agent Platform w Google Cloud) albo CLAUDE_CODE_USE_FOUNDRY=1 razem z poświadczeniami danego dostawcy. Przegląd Agent SDK mówi też, że zewnętrzni deweloperzy nie mogą bez wcześniejszej zgody oferować logowania przez claude.ai ani jego limitów w produktach zbudowanych na SDK, więc produkt, który udostępniasz innym, uwierzytelnia się kluczem API.
Jak SDK steruje pętlą Claude Code
Dział zatytułowany „Jak SDK steruje pętlą Claude Code”Każde wywołanie SDK uruchamia binarkę claude, wysyła twój prompt strumieniem JSON i zwraca typowane komunikaty: SystemMessage, AssistantMessage (tekst i bloki wywołań narzędzi), UserMessage (wyniki narzędzi) oraz jeden ResultMessage na koniec każdej tury. To na ResultMessage reaguje twój kod. Zawiera subtype (success, error_max_turns, error_max_budget_usd, error_max_structured_output_retries albo error_during_execution), is_error, result, structured_output, total_cost_usd, num_turns, session_id i permission_denials.
W obu językach są dwa punkty wejścia:
query()to jedno wywołanie: prompt na wejściu, strumień komunikatów na wyjściu. Nadaje się do zadań wsadowych, w których znasz całe wejście z góry.ClaudeSDKClient(Python) trzyma otwarte połączenie, więc możesz wysyłać kolejne wiadomości, przerwać turę przezinterrupt()i zmienić tryb uprawnień w trakcie sesji. README Pythona przedstawia go jako punkt wejścia dla własnych narzędzi i hooków, dlatego przykład poniżej go używa. W TypeScripciequery()zwraca obiektQuery, który robi to samo, gdy jako prompt przekażesz asynchroniczny iterable.
Trzy ustawienia domyślne zaskakują ludzi przychodzących z CLI:
- Bez promptu systemowego Claude Code, dopóki o niego nie poprosisz. Gdy pominiesz
system_prompt, oba SDK uruchamiają CLI z pustym promptem systemowym (sprawdzone w kodzie 0.2.160 i 0.3.283). Dla agenta programistycznego przekaż{"type": "preset", "preset": "claude_code", "append": "..."}. - Wczytuje się każdy plik ustawień, dopóki tego nie zawęzisz. Nieustawione
setting_sourceswczytuje ustawienia użytkownika, projektu i lokalne, więc własny~/.claude/settings.jsonrunnera przecieka do przebiegu. Przekaż["project"]tylko wtedy, gdy kod wcwdjest zaufany; lista musi zawierać"project", żeby wczytał sięCLAUDE.md. Gdycwdto commit kontrybutora, przekaż[]: ustawienia projektu mogą definiować hooki, a hooki uruchamiają polecenia powłoki z sekretami joba w środowisku. - Sesje SDK nie startują w trybie auto. Od v2.1.283 (kanał
latest) auto mode jest trybem startowym sesji interaktywnych, aleclaude -pi Agent SDK nadal startują w domyślnym, ręcznym trybie. Ustawpermission_modejawnie.
Zbuduj bota do triage’u CI na Agent SDK
Dział zatytułowany „Zbuduj bota do triage’u CI na Agent SDK”Bot uruchamia się po nieudanym workflow CI, czyta log z nieudanych kroków i kod z checkoutu, a potem zapisuje verdict.json. Kolejny krok workflow zamienia ten plik w komentarz pod PR. Sam agent nie może ani zapisywać plików, ani uruchamiać poleceń.
-
Napisz kontrakt przed promptem. Werdykt to JSON Schema:
categoryz zamkniętej listy,culprit_file, niepusta tablicaevidence,confidenceisummary. Przekazany jakooutput_formatsprawia, że CLI waliduje końcową odpowiedź i ponawia próbę, aż będzie zgodna. Jeśli się nie da, tura kończy sięerror_max_structured_output_retries, a nie wolnym tekstem, który późniejszy krok musiałby parsować. -
Usuń narzędzia, których bot nigdy nie może mieć.
tools=["Read", "Grep", "Glob", "Agent"]to kompletny zestaw wbudowanych narzędzi tej sesji.Bash,EditiWritenie są ograniczone; dla modelu po prostu nie istnieją.allowed_toolsto coś innego: tylko automatycznie zatwierdza i nic nie mówi o dostępności. -
Zatwierdzaj trybem, nie listą.
permission_mode="dontAsk"odrzuca wszystko, czego nie zatwierdza żadna reguła, i nigdy nie czeka na pytanie. Claude Code zatwierdza odczyty plików iGrepwewnątrz katalogu roboczego bez żadnej reguły, więcReadzostaje pozaallowed_tools. To samo dotyczyAgent, który nigdy nie pyta przed uruchomieniem, więc jedyną regułą allow jest własne narzędzie do logu. GołeReadjako reguła allow zatwierdziłoby odczyty w dowolnym miejscu runnera, także poza checkoutem. -
Daj agentowi wąskie narzędzie zamiast powłoki.
get_failed_logto funkcja w Pythonie wystawiona jako narzędzie MCP w procesie. Agent może pobrać log z nieudanych kroków tego przebiegu i nic więcej. Nie uruchomighz innymi argumentami, bo nie maBash. -
Regułę, którą model mógłby pominąć, wymuś hookiem. Odczyty wewnątrz checkoutu są zatwierdzane automatycznie, więc hook
PreToolUseodrzuca każdeRead,GrepiGlob, którego ścieżka, filtr glob albo wzorzecGlobwygląda jak.env*albosecrets/. Hooki są pierwsze w kolejności oceny uprawnień, a odmowa z hooka obowiązuje w każdym trybie. Hook to zabezpieczenie awaryjne, a nie właściwa ochrona:Grepbez ścieżki przeszukuje całycwdi hook nie ma czego dopasować. Prawdziwą ochroną jest checkout, w którym nie ma żadnych sekretów. -
Trzymaj log poza głównym kontekstem dzięki subagentowi. Subagent
log-readerna tańszym aliasiehaikuczyta do 3000 linii logu i zwraca tylko pierwszy prawdziwy błąd z 20 liniami kontekstu. Główny agent wydaje kontekst na kod, nie na kaskadę błędów. -
Ogranicz przebieg i sprawdź odpowiedź.
max_turns=30imax_budget_usd=2.0ograniczają koszt zagubionego przebiegu. Po otrzymaniu wyniku zwykły Python sprawdza każdy dowód w logu i w checkoucie, a jeśli któryś nie ma pokrycia, obniża werdykt dounknown.
# .github/scripts/triage.py: explains why a CI run failed. Read-only by construction.import jsonimport osimport reimport sysfrom pathlib import Path
import anyiofrom claude_agent_sdk import ( AgentDefinition, ClaudeAgentOptions, ClaudeSDKClient, HookMatcher, ResultMessage, create_sdk_mcp_server, tool,)
RUN_ID = os.environ["RUN_ID"]REPO = Path(os.environ.get("TARGET_DIR", ".")).resolve() # the untrusted checkout: data, never code
async def failed_log() -> str: proc = await anyio.run_process(["gh", "run", "view", RUN_ID, "--log-failed"]) return proc.stdout.decode(errors="replace")
# 1. A custom tool: the agent gets the failed-step log, not a shell.@tool("get_failed_log", "Log of the failed steps in the CI run under triage", {"max_lines": int})async def get_failed_log(args): lines = (await failed_log()).splitlines() return {"content": [{"type": "text", "text": "\n".join(lines[-args["max_lines"]:])}]}
# 2. A hook: runs on every matching call, whatever the model decides.async def deny_secret_reads(input_data, tool_use_id, context): tool_input = input_data["tool_input"] keys = ["file_path", "path", "glob"] + (["pattern"] if input_data["tool_name"] == "Glob" else []) # A Grep over the whole cwd has no path to match: the checkout itself must hold no secrets. if any(re.search(r"(^|/)(\.env[^/]*|secrets?)(/|$)", str(tool_input.get(k) or "")) for k in keys): return { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Triage never reads secret files.", } } return {}
# 3. The verdict contract.VERDICT = { "type": "object", "properties": { "category": {"enum": ["test-regression", "flaky-test", "build-config", "infrastructure", "unknown"]}, "culprit_file": {"type": ["string", "null"]}, "evidence": {"type": "array", "items": {"type": "string"}, "minItems": 1}, "confidence": {"enum": ["high", "medium", "low"]}, "summary": {"type": "string"}, }, "required": ["category", "culprit_file", "evidence", "confidence", "summary"], "additionalProperties": False,}
options = ClaudeAgentOptions( system_prompt={"type": "preset", "preset": "claude_code", "append": "You are a CI triage bot. You never change files."}, cwd=REPO, tools=["Read", "Grep", "Glob", "Agent"], # the entire built-in toolset: no Bash, Edit or Write allowed_tools=["mcp__ci__get_failed_log"], # reads inside cwd and Agent calls need no rule permission_mode="dontAsk", # anything not approved is denied, never prompted setting_sources=[], # load NO settings from the untrusted checkout: its hooks would run with your API key mcp_servers={"ci": create_sdk_mcp_server(name="ci", version="1.0.0", tools=[get_failed_log])}, strict_mcp_config=True, # ignore any .mcp.json the checked-out branch brings along agents={ "log-reader": AgentDefinition( description="Reads the CI failure log and returns only the first real error.", prompt=( "Call get_failed_log with max_lines=3000. Return the first error that is not a " "consequence of an earlier one, quoted verbatim with 20 lines of context, and the " "test or step name. Do not speculate about causes." ), tools=["mcp__ci__get_failed_log"], model="haiku", ) }, hooks={"PreToolUse": [HookMatcher(matcher="Read|Grep|Glob", hooks=[deny_secret_reads])]}, output_format={"type": "json_schema", "schema": VERDICT}, max_turns=30, max_budget_usd=2.0,)
PROMPT = ( "CI run failed. Use the log-reader subagent to get the first real error, then find the code it " "points at. Classify the failure. Every evidence item must be a verbatim log line or a " "path:line you opened. If you cannot tie the error to a file, answer unknown; do not guess.")
# 4. Deterministic check: every evidence item must exist in the log or in the checkout.MIN_EVIDENCE = 20 # "Error" or "FAIL" appear in every failed log and prove nothing
def grounded(item: str, log: str) -> bool: item = item.strip() if len(item) >= MIN_EVIDENCE and item in log: return True m = re.fullmatch(r"([\w./-]+):(\d+)", item) if not m: return False path = (REPO / m[1]).resolve() if not (path.is_file() and path.is_relative_to(REPO)): return False return int(m[2]) <= len(path.read_text(errors="replace").splitlines())
async def main() -> int: async with ClaudeSDKClient(options=options) as client: await client.query(PROMPT) async for msg in client.receive_response(): if not isinstance(msg, ResultMessage): continue print(f"subtype={msg.subtype} cost_usd={msg.total_cost_usd} turns={msg.num_turns}", file=sys.stderr) if msg.is_error or msg.structured_output is None: return 1 # no verdict, no comment verdict = msg.structured_output log = await failed_log() if not all(grounded(e, log) for e in verdict["evidence"]): verdict |= {"category": "unknown", "confidence": "low"} Path("verdict.json").write_text(json.dumps(verdict, indent=2)) return 0 return 1
if __name__ == "__main__": sys.exit(anyio.run(main))// .github/scripts/triage.mts: the same bot, including the grounding check.import { execFile } from 'node:child_process';import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';import { writeFile } from 'node:fs/promises';import { resolve, sep } from 'node:path';import { promisify } from 'node:util';import { createSdkMcpServer, query, tool, type HookCallback } from '@anthropic-ai/claude-agent-sdk';import { z } from 'zod';
const run = promisify(execFile);const RUN_ID = process.env.RUN_ID!;const REPO = realpathSync(resolve(process.env.TARGET_DIR ?? '.')); // the untrusted checkout: data, never code
async function failedLog(): Promise<string> { const { stdout } = await run('gh', ['run', 'view', RUN_ID, '--log-failed'], { maxBuffer: 64 * 1024 * 1024 }); return stdout;}
const getFailedLog = tool( 'get_failed_log', 'Log of the failed steps in the CI run under triage', { max_lines: z.number().int().positive() }, async ({ max_lines }) => ({ content: [{ type: 'text', text: (await failedLog()).split('\n').slice(-max_lines).join('\n') }], }),);
const denySecretReads: HookCallback = async (input) => { if (input.hook_event_name !== 'PreToolUse') return {}; const { file_path, path, glob, pattern } = input.tool_input as Record<string, string | undefined>; const targets = [file_path, path, glob, input.tool_name === 'Glob' ? pattern : undefined]; // A Grep over the whole cwd has no path to match: the checkout itself must hold no secrets. if (targets.some((t) => /(^|\/)(\.env[^/]*|secrets?)(\/|$)/.test(t ?? ''))) { return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'Triage never reads secret files.', }, }; } return {};};
const verdictSchema = { type: 'object', properties: { category: { enum: ['test-regression', 'flaky-test', 'build-config', 'infrastructure', 'unknown'] }, culprit_file: { type: ['string', 'null'] }, evidence: { type: 'array', items: { type: 'string' }, minItems: 1 }, confidence: { enum: ['high', 'medium', 'low'] }, summary: { type: 'string' }, }, required: ['category', 'culprit_file', 'evidence', 'confidence', 'summary'], additionalProperties: false,};
// Deterministic check: every evidence item must exist in the log or in the checkout.const MIN_EVIDENCE = 20; // "Error" or "FAIL" appear in every failed log and prove nothing
function grounded(item: string, log: string): boolean { const s = item.trim(); if (s.length >= MIN_EVIDENCE && log.includes(s)) return true; const m = /^([\w./-]+):(\d+)$/.exec(s); if (!m || !existsSync(resolve(REPO, m[1]))) return false; const path = realpathSync(resolve(REPO, m[1])); if (!path.startsWith(REPO + sep) || !statSync(path).isFile()) return false; return Number(m[2]) <= readFileSync(path, 'utf8').replace(/\n$/, '').split('\n').length;}
for await (const msg of query({ prompt: 'CI run failed. Use the log-reader subagent to get the first real error, then find the code it points at. ' + 'Classify the failure. Every evidence item must be a verbatim log line or a path:line you opened. ' + 'If you cannot tie the error to a file, answer unknown; do not guess.', options: { systemPrompt: { type: 'preset', preset: 'claude_code', append: 'You are a CI triage bot. You never change files.' }, cwd: REPO, tools: ['Read', 'Grep', 'Glob', 'Agent'], allowedTools: ['mcp__ci__get_failed_log'], // reads inside cwd and Agent calls need no rule permissionMode: 'dontAsk', settingSources: [], // load NO settings from the untrusted checkout: its hooks would run with your API key mcpServers: { ci: createSdkMcpServer({ name: 'ci', version: '1.0.0', tools: [getFailedLog] }) }, strictMcpConfig: true, agents: { 'log-reader': { description: 'Reads the CI failure log and returns only the first real error.', prompt: 'Call get_failed_log with max_lines=3000. Return the first error that is not a consequence of an ' + 'earlier one, quoted verbatim with 20 lines of context, and the test or step name. Do not speculate.', tools: ['mcp__ci__get_failed_log'], model: 'haiku', }, }, hooks: { PreToolUse: [{ matcher: 'Read|Grep|Glob', hooks: [denySecretReads] }] }, outputFormat: { type: 'json_schema', schema: verdictSchema }, maxTurns: 30, maxBudgetUsd: 2, },})) { if (msg.type === 'result') { console.error(`cost=$${msg.total_cost_usd} turns=${msg.num_turns} subtype=${msg.subtype}`); if (msg.subtype !== 'success' || msg.is_error || msg.structured_output == null) process.exit(1); const verdict = msg.structured_output as { category: string; confidence: string; evidence: string[] }; const log = await failedLog(); if (!verdict.evidence.every((e) => grounded(e, log))) Object.assign(verdict, { category: 'unknown', confidence: 'low' }); await writeFile('verdict.json', JSON.stringify(verdict, null, 2)); }}Ta wersja przechodzi tsc --strict względem 0.3.283.
Nazwa narzędzia MCP ma postać mcp__<server>__<tool>, gdzie <server> to klucz w mcp_servers (ci), a nie name przekazane do create_sdk_mcp_server. Pomyl klucz, a allowed_tools zatwierdzi narzędzie, które nie istnieje, i dontAsk po cichu je odrzuci.
Uruchom bota do triage’u w GitHub Actions
Dział zatytułowany „Uruchom bota do triage’u w GitHub Actions”Workflow odpala się, gdy twój workflow CI kończy się porażką. Uruchamia skrypt triage z gałęzi domyślnej, robi checkout nieudanego commita do osobnego katalogu untrusted/, który agent tylko czyta, i publikuje werdykt tylko wtedy, gdy skrypt go wyprodukował.
name: ci-triageon: workflow_run: workflows: [CI] types: [completed]permissions: actions: read contents: read pull-requests: writejobs: triage: if: github.event.workflow_run.conclusion == 'failure' runs-on: ubuntu-latest timeout-minutes: 15 env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} steps: # Trusted code: the triage script comes from the default branch, never from the failing commit. - uses: actions/checkout@v7 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false # Untrusted data: the failing commit, checked out beside it and only ever read by the agent. - uses: actions/checkout@v7 with: ref: ${{ github.event.workflow_run.head_sha }} path: untrusted persist-credentials: false - uses: actions/setup-python@v7 with: python-version: '3.12' - run: pip install claude-agent-sdk==0.2.160 - name: Triage env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} RUN_ID: ${{ github.event.workflow_run.id }} TARGET_DIR: untrusted run: python .github/scripts/triage.py - name: Comment on the pull request if: github.event.workflow_run.pull_requests[0] != null run: | jq -r '"**CI triage:** \(.category) (\(.confidence) confidence)\n\n\(.summary)\n\n" + (.evidence | map("- `" + . + "`") | join("\n"))' verdict.json > comment.md gh pr comment ${{ github.event.workflow_run.pull_requests[0].number }} --body-file comment.mdDla pull requestów z forków workflow_run.pull_requests jest puste, więc ten krok się pomija; jeśli chcesz tam skomentować, znajdź PR przez gh pr list --search <head_sha> --state open --json number.
workflow_run działa z plikiem workflow i sekretami z gałęzi domyślnej, a nieudany commit może pochodzić od dowolnego kontrybutora. Dlatego nic z tego commita nigdy się nie wykonuje: skrypt pochodzi z gałęzi domyślnej, setting_sources=[] nie ładuje hooków z .claude/settings.json ani .mcp.json z commita, a agent nie dostaje Bash, narzędzi zapisu ani poświadczeń w .git/config. Uruchomienie skryptu albo załadowanie ustawień z nieudanego commita przy ANTHROPIC_API_KEY w środowisku pozwoliłoby dowolnemu pull requestowi wykraść klucz. Zanim poluzujesz którekolwiek z tych ograniczeń, przeczytaj model zagrożeń dla agentów.
Jak łączą się opcje uprawnień w Agent SDK?
Dział zatytułowany „Jak łączą się opcje uprawnień w Agent SDK?”Opcje na siebie zachodzą i większość błędów w uprawnieniach bierze się z traktowania jednej jak drugiej. Claude Code ocenia każde wywołanie narzędzia w tej kolejności: hooki, reguły deny, reguły ask, tryb uprawnień, reguły allow, a na końcu twój callback can_use_tool.
| Opcja (Python / TypeScript) | Co robi | Typowy błąd |
|---|---|---|
tools / tools | Ustala bazowy zestaw wbudowanych narzędzi. [] wyłącza wszystkie | Pozostawienie jej nieustawionej i próba ograniczania przez allowed_tools |
allowed_tools / allowedTools | Reguły allow: wymienione wywołania są zatwierdzane automatycznie | Przekonanie, że ukrywa niewymienione narzędzia. README: „nie usuwa narzędzi z zestawu Claude’a” |
disallowed_tools / disallowedTools | Reguły deny. Goła nazwa (Bash) usuwa narzędzie; reguła z zakresem (Bash(rm *)) blokuje pasujące wywołania w każdym trybie | Oczekiwanie, że Bash(rm *) złapie /bin/rm; reguła dopasowuje polecenie w zapisanej postaci |
permission_mode / permissionMode | default, acceptEdits, plan, dontAsk, auto, bypassPermissions | bypassPermissions w CI. TypeScript wymaga do niego dodatkowo allowDangerouslySkipPermissions: true |
hooks / hooks | Twoja funkcja działa przed regułami. Odmowa obowiązuje w każdym trybie; zgoda nie pomija późniejszych reguł deny ani ask | Założenie, że zgoda z hooka przebija regułę deny |
can_use_tool / canUseTool | Rozstrzyga wywołania, których nic wcześniej nie rozstrzygnęło, zamiast interaktywnego pytania | Oczekiwanie, że widzi każde wywołanie. Nigdy nie widzi wywołań zatwierdzonych wcześniej, a dontAsk całkiem go pomija |
Dla bota bez nadzoru niezawodny jest wzorzec z przykładu: zmniejsz tools, wybierz dontAsk, zatwierdź tylko dodatkowe narzędzia potrzebne do zadania, a każdą regułę „nigdy” umieść w hooku albo w regule deny. Dla bota, przy którym człowiek jest osiągalny (np. zatwierdzający na Slacku), użyj trybu default z callbackiem can_use_tool, który przekazuje prośbę dalej i czeka na odpowiedź.
Hooki różnią się między SDK. SDK TypeScriptu przyjmuje pełny zestaw zdarzeń Claude Code; SDK Pythona przyjmuje podzbiór (PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStart, SubagentStop, PreCompact, Notification i PermissionRequest w 0.2.160), bez SessionStart i SessionEnd. Jeśli twój projekt zależy od hooka sesji, pisz w TypeScripcie. Hooki w postaci poleceń powłoki, współdzielone z pracą interaktywną, opisuje przewodnik po hookach Claude Code.
Sesje: wznawianie, forkowanie i dlaczego CI je gubi
Dział zatytułowany „Sesje: wznawianie, forkowanie i dlaczego CI je gubi”Każdy ResultMessage niesie session_id. Przekaż go z powrotem jako resume, żeby kontynuować z pełnym kontekstem, a dodaj fork_session=True, żeby odgałęzić nową sesję z nowym ID bez zmiany oryginału. To otwiera wzorzec dwufazowy: sesja triage’u tylko do odczytu, potem jej fork w trybie acceptEdits, który przygotowuje poprawkę na gałęzi i korzysta ze wszystkiego, co triage już przeczytał.
Haczyk leży w tym, gdzie mieszkają sesje. Claude Code zapisuje transkrypty w ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl na maszynie, która je utworzyła, więc sesja z jednego joba GitHub Actions znika, gdy następny job startuje na świeżym runnerze. Masz trzy wyjścia:
- Uruchom obie fazy w jednym jobie.
- Wgraj plik
.jsonljako artefakt i odtwórz go w~/.claude/projects/przed wywołaniemresume. - Podłącz adapter
session_store(Python) albosessionStore(TypeScript), który kopiuje transkrypty do twojego magazynu, i wznawiaj z tym samymcwd.
Przy przebiegach „odpal i zapomnij”, których nikt nie będzie wznawiał, persistSession: false w TypeScripcie w ogóle pomija zapis transkryptu. W Pythonie ustaw zamiast tego CLAUDE_CODE_SKIP_PROMPT_HISTORY w opcji env (dokumentacja sesji Agent SDK).
Jak udowodnić, że bot do triage’u ma rację?
Dział zatytułowany „Jak udowodnić, że bot do triage’u ma rację?”Bot jest przydatny tylko wtedy, gdy jego werdykty trafiają częściej niż zmęczony człowiek przeglądający log, a ty musisz to wiedzieć bez czytania każdego werdyktu. Kontrolę robią cztery warstwy i każda zawodzi głośno.
- Schemat. CLI waliduje werdykt względem JSON Schema. Przebieg, który nie potrafi go wyprodukować, kończy się niezerowym kodem i niczego nie publikuje.
- Ugruntowanie.
grounded()sprawdza, czy każdy dowód ma co najmniej 20 znaków i występuje dosłownie w logu, albo jest istniejącympath:linewewnątrz checkoutu. Wymyślony plik albo sparafrazowana linia logu zamienia werdykt wunknownz niską pewnością. To zwykły kod, więc możesz go przetestować jednostkowo. - Limity budżetu i tur. Zagubiony przebieg kończy się
error_max_budget_usdalboerror_max_turnszamiast przepalać pieniądze; koszt każdego przebiegu trafia ztotal_cost_usddo logu joba. - Zestaw ewaluacyjny. Oznacz 20 historycznych nieudanych przebiegów ich znaną przyczyną, puść na nich bota i porównaj. Uruchom to lokalnie, z własnym kluczem API:
# evals/triage-cases.tsv holds lines of: <run id> <TAB> <expected category># Each case gets its own worktree; triage.py always comes from your current branch.while IFS=$'\t' read -r run expected; do sha=$(gh run view "$run" --json headSha -q .headSha) # Fork heads and force-pushed commits are often missing from a local clone. git fetch --quiet origin "$sha" || { echo -e "$run\t$expected\tskip\tFETCH-FAIL"; continue; } git worktree add --quiet --detach "/tmp/case-$run" "$sha" rm -f verdict.json if RUN_ID="$run" TARGET_DIR="/tmp/case-$run" python .github/scripts/triage.py 2>>costs.log; then got=$(jq -r .category verdict.json) else got=no-verdict fi git worktree remove --force "/tmp/case-$run" echo -e "$run\t$expected\t$got\t$([ "$got" = "$expected" ] && echo PASS || echo FAIL)"done < evals/triage-cases.tsv | tee results.tsvawk -F'\t' '$4=="PASS"{p++} END{print p+0"/"NR" passed"}' results.tsvPowtarzaj ewaluację przy każdej zmianie promptu, schematu, modelu subagenta albo wersji SDK i trzymaj wyniki razem ze zmianą. Jeśli już używasz promptfoo, jego provider anthropic:claude-agent-sdk uruchamia SDK jako testowany system w ten sam sposób.
Kto zatwierdza: tech lead odpowiedzialny za CI ustala próg trafności, od którego komentarze bota są wiarygodne, a dopóki ewaluacja go nie przekroczy, komentarz pozostaje doradczy i nigdy nie blokuje merge’a. Gdy werdykt okaże się błędny na produkcji, dopisz ten przebieg do triage-cases.tsv. To nawyk dowodów zamiast diffów zastosowany do agenta: czytasz tabelę ewaluacji i wynik ugruntowania, a nie transkrypt agenta.
Prompty do skopiowania do pracy z Agent SDK
Dział zatytułowany „Prompty do skopiowania do pracy z Agent SDK”Wklej je do Claude Code w repozytorium, w którym ma działać bot.
Co się psuje, gdy sterujesz Claude Code z Agent SDK?
Dział zatytułowany „Co się psuje, gdy sterujesz Claude Code z Agent SDK?”| Objaw | Przyczyna | Wyjście |
|---|---|---|
| Agent zachowuje się jak zwykły chatbot i ignoruje konwencje narzędzi | Pominięty system_prompt, więc CLI wystartowało z pustym promptem | Przekaż preset claude_code z append |
Reguły z CLAUDE.md są ignorowane w CI albo pojawiają się prywatne ustawienia dewelopera | setting_sources nie zawiera "project" albo jest nieustawione i wczytuje ustawienia użytkownika runnera | Dla zaufanych checkoutów ustaw jawnie ["project"]; dla niezaufanych zostaw [] i przekaż reguły w system_prompt |
Narzędzie z allowed_tools nigdy nie jest wywołane; permission_denials je wymienia | Nazwa narzędzia MCP używa name serwera, a nie jego klucza w mcp_servers | Zmień na mcp__<key>__<tool> |
| Przebieg wisi albo każdy zapis jest odrzucany w CI | Nie ma człowieka, który odpowie na pytania trybu domyślnego | Użyj dontAsk i zatwierdź to, czego potrzebuje zadanie, albo dostarcz can_use_tool |
subtype to error_max_structured_output_retries | Schemat jest ostrzejszy, niż pozwalają dowody, np. culprit_file wymagany jako string, gdy go nie ma | Dopuść null albo kategorię unknown, jak w przykładzie |
resume w kolejnym jobie zgłasza nieznaną sesję | Transkrypty są lokalne dla runnera, który je utworzył | Ten sam job, odtworzony artefakt .jsonl albo magazyn sesji |
TypeScript: „Claude Code native binary not found at …”; Python: CLINotFoundError | Brak działającej binarki CLI: w TypeScripcie pominięto zależności opcjonalne, więc brak binarki dla platformy | Instaluj bez --omit=optional albo ustaw pathToClaudeCodeExecutable (TypeScript) lub cli_path (Python) |
| Gałąź kontrybutora dodaje serwer MCP, który bot potem wczytuje | Projektowy .mcp.json wczytuje się w sesjach SDK bez pytania | strict_mcp_config=True i serwery przekazywane wyłącznie w kodzie |
Narzędzie do logu zawodzi (np. brak gh na runnerze) | Środowisko, nie agent | Prompt każe agentowi odpowiedzieć unknown; przebieg testowy z 2026-09-26 bez gh zrobił dokładnie to zamiast zgadywać. Napraw runner i uruchom ponownie |
Dokąd dalej z Agent SDK
Dział zatytułowany „Dokąd dalej z Agent SDK”Model stojący za aliasem haiku i aktualne ceny znajdziesz w centrum wiedzy o modelach. Serwery MCP inne niż te działające w procesie opisuje konfiguracja MCP w Claude Code. Odpowiednikiem dla Codexa jest praca z Codex SDK, a dla Cursora Cursor SDK.