본문 바로가기
MegazoneCloud
← 전체 글
Claude Code Codex Kiro

headless로 이런 것도 된다: Claude Code · Codex · Kiro CLI, 그리고 서로 부르게 하기

headless로 이런 것도 된다: Claude Code · Codex · Kiro CLI, 그리고 서로 부르게 하기

한 터미널의 세 항공사

그림: 한 터미널의 세 항공사 — 한 기체의 호출이 다른 두 기체로 닿는다. AI(Gemini)로 생성한 그림입니다.

0. 세 줄 요약

  1. AI 에이전트 CLI 3종(Claude Code · Codex · Kiro CLI)은 headless로 부를 수 있다 — 대화창 없이 한 번 실행하고 답만 받는다. 개념은 같고 플래그만 달라서, 이 글은 셋을 같은 순서로 나란히 비교한다.
  2. 그런데 CLI는 AI 에이전트가 가장 잘 다루는 도구다. 즉, AI 에이전트가 다른 프로바이더의 AI 에이전트를 직접 부를 수 있다.
  3. 그러니 headless 호출을 스킬·에이전트로 포장하면 하나의 CLI 안에서 멀티 프로바이더가 된다 — Claude Code에서 각각 /ask-codex, /ask-kiro 한 단어로 가능하다.

1. 들어가며

1.1 headless가 뭔가, 그리고 이 글의 방향

headless는 대화창 없이 한 번 실행하고 답만 받는 방식이다. 셸 파이프(|)·스크립트·다른 프로그램에서 부를 수 있고, 그 "다른 프로그램"에는 다른 AI CLI도 포함된다.

CLI는 AI 에이전트가 가장 잘 다루는 도구다. 따라서 AI 에이전트는 CLI를 통해 다른 프로바이더의 AI 에이전트를 호출하고 적절하게 쓸 수 있다.

즉, Claude Code 안에서 Codex와 Kiro를 부를 수 있게 된다. 지금 내 Claude Code에서는 이 한 줄이 된다.

$ claude -p "/ask-codex hello.py를 세 줄 이내로 리뷰해" < /dev/null
- `greet`는 간결하지만 문자열 보간(`f"Hello, {name}"`)이 더 읽기 좋습니다.
- 인자 없이 실행하면 `IndexError`가 발생하므로 사용법 안내나 인자 검증이 필요합니다.
- 기본 동작을 확인하는 간단한 테스트를 추가하면 좋습니다.

Claude Code 안에서 Codex CLI가 headless로 돌고, 그 답만 돌아온다. /ask-kiro도 같은 방식이다.

$ claude -p "/ask-kiro hello.py를 한 문장으로 요약해" < /dev/null
이름을 받아 "Hello, {이름}" 인사말을 만드는 greet 함수를 정의하고, 스크립트로 직접 실행하면 첫 번째 명령행 인자를 이름으로 써서 인사말을 출력하는 파일입니다.

이 글의 방향은 두 단계다. 먼저 세 CLI — Claude Code, Codex, Kiro CLI — 의 headless 사용법을 같은 순서로 자세히 정리한다(2~7장). 그다음 그 재료로 커스텀 스킬과 에이전트를 만들어, 하나의 프로바이더 안에서 여러 프로바이더를 쓰는 조합을 제안한다(8장).

굳이 직접 만드는 이유는 공식 지원이 갈라져 있어서다.

벤더 공식 크로스 프로바이더 지원 방식
OpenAI 있음 Claude Code 마켓플레이스에 공식 Codex 플러그인 배포 (openai/codex-plugin-cc)
Anthropic 없음 역방향(다른 하네스가 Claude Code를 부르는 것) 지원 요청이"not planned"로 종료. headless claude -p와 agent sdk만 공식
AWS (Kiro) 프로토콜로 ACP 채택 — 에디터 등 ACP 클라이언트가 Kiro를 부를 수 있게 함

Kiro의 ACP는 "Kiro가 다른 CLI를 부른다"가 아니라 "ACP 클라이언트가 Kiro를 부를 수 있다"는 피호출 방향이다. 결국 이 세 CLI 사이에서, 벤더가 만들어 배포하는 크로스 프로바이더 통합은 Codex 플러그인 하나뿐이다. 나머지 조합은 각 CLI가 공식 지원하는 headless 모드를 직접 조합하는 수밖에 없고, 그게 이 글이 headless를 파는 이유다.

1.2 세 항공사, 같은 비행 절차

이 글은 세 CLI를 같은 순서로 나란히 본다. 항공사에 비유하면 이유가 분명해진다. 항공사가 달라도 비행 절차는 같다 — 체크인을 하고(준비), 표를 끊고(호출), 좌석 등급을 고르고(모델·에포트), 전담 승무원이 배정되고(에이전트), 표준 절차 카드가 실리고(스킬), 보안검색을 통과한다(도구 권한). 다른 것은 안내판의 표기뿐이다. 개념은 같고 플래그만 다르다.

세 항공사의 같은 탑승 절차

그림: 세 항공사의 같은 탑승 절차 — 다른 것은 색과 표기뿐. AI(Gemini)로 생성한 그림입니다.

아래 대응표가 이 글 전체의 지도 — 말하자면 세 공항의 안내판 대조표다.

개념 Claude Code Codex Kiro CLI
headless 스위치 claude -p "..." codex exec "..." kiro-cli chat --no-interactive "..."
모델 --model sonnet -m gpt-5.6-sol --model claude-sonnet-5
에포트 --effort low -c model_reasoning_effort='"low"' --effort low
에이전트 정의 .claude/agents/<name>.md + --agent <name> .codex/agents/<name>.toml (프롬프트로 spawn) .kiro/agents/<name>.json + --agent <name>
스킬 정의 .claude/skills/<name>/SKILL.md, 프롬프트 /<name> .agents/skills/<name>/SKILL.md, 프롬프트 $<name> .kiro/skills/<name>/SKILL.md, 프롬프트에 이름
도구 허용 --allowedTools "Bash(python3 *)" -s read-only|workspace-write --trust-tools=fs_read / --trust-all-tools
JSON 출력 --output-format json / stream-json --verbose --json / -o file / --output-schema --output-format stream-json --agent-engine v2
폴더 밖 읽기 --add-dir ../x --add-dir (에이전트 resources)

1.3 이 글의 실험 조건

전역 설정이 보이지 않는 격리 환경에서 실행했으므로 독자 머신의 기본 상태와 같다. 버전은 claude 2.1.259 · codex-cli 0.151.0 · kiro-cli 2.20.1. 검증한 호출 방향은 Claude Code → Codex/Kiro 하나이고, 반대 방향은 범위 밖이다. 모든 명령은 독자가 그대로 칠 수 있는 형태다.

2. 준비

체크인 카운터부터. 설치 확인은 세 CLI 모두 --version이다.

$ claude --version
$ codex --version
$ kiro-cli --version
2.1.259 (Claude Code)
codex-cli 0.151.0
kiro-cli 2.20.1

Kiro는 로그인 상태와 모델 목록을 CLI가 직접 알려준다는 점이 준비 단계부터 다르다.

$ kiro-cli whoami
Logged in with IAM Identity Center (https://<idc-id>.awsapps.com/start)
Email: <email>

Profile:
QDefaultProfile
<arn>
$ kiro-cli chat --list-models
Available models (* = default):

  auto                 1.00x credits      Models chosen by task for optimal usage and consistent quality
* claude-opus-5        2.20x credits      Claude Opus 5 model with 1M context window
  claude-sonnet-5      1.30x credits      Claude Sonnet 5 model with 1M context window
  claude-opus-4.8      2.20x credits      Claude Opus 4.8 model with 1M context window
  gpt-5.6-sol          2.40x credits      Experimental preview of OpenAI GPT 5.6 Sol with 272k context window
  gpt-5.6-terra        1.00x credits      Experimental preview of OpenAI GPT 5.6 Terra with 272k context window
  gpt-5.6-luna         0.10x credits      Experimental preview of OpenAI GPT 5.6 Luna with 272k context window
...
  claude-haiku-4.5     0.40x credits      The latest Claude Haiku model
...

실험 대상은 인자 검사 없는 7줄짜리 hello.py 하나를 세 폴더에 똑같이 뒀다.

def greet(name):
    return "Hello, " + name


if __name__ == "__main__":
    import sys
    print(greet(sys.argv[1]))

3. 그냥 부르기

headless 호출은 경유(transit)다. 대화형 세션이 입국해서 머무는 여행이라면, headless는 입국 없이 한 번 통과하며 답만 들고 나온다. 세 공항의 경유 절차를 차례로 밟아 보자.

대화형은 입국, headless는 경유

그림: 대화형은 입국, headless는 경유 — 머무르지 않고 답만 들고 나온다. AI(Gemini)로 생성한 그림입니다.

출발 전에 stdin 한 가지. 프롬프트는 셋 다 인자로 받지만, stdin이 터미널이 아닐 때(스크립트·파이프·다른 에이전트의 셸) 동작이 다르다. Claude Code는 3초 기다렸다 경고를 내고 진행하고, Codex는 EOF까지 기다리며, Kiro는 프롬프트를 인자로 주면 stdin을 보지 않는다. 그래서 이 글의 claude 명령에는 전부 < /dev/null을 붙였다 — 대기와 경고가 사라진다. 파이프·스크립트에서 Codex를 쓸 때도 < /dev/null이 필수다. 터미널에서 직접 칠 땐 필요 없다. (이 대기 동작만 본문의 다른 기록과 달리 2026-09-07, claude 2.1.261 · codex-cli 0.153.4 · kiro-cli 2.21.1에서 확인했다.)

3.1 Claude Code: -p

-p(--print)는 답만 출력하고 종료한다. 현재 폴더의 파일 읽기는 승인 없이 된다.

$ claude -p "hello.py가 무엇을 하는지 한 문장으로 설명해" < /dev/null
`hello.py`는 이름을 받아 `"Hello, <이름>"` 문자열을 만드는 `greet()` 함수를 정의하고, 스크립트로 직접 실행하면 첫 번째 명령줄 인자를 그 함수에 넘겨 인사말을 출력합니다.

3.2 Codex: codex exec

첫 실행부터 거부당했다. git 저장소가 아닌 폴더에서는 --skip-git-repo-check가 없으면 실행하지 않는다.

$ codex exec "hello.py가 무엇을 하는지 한 문장으로 설명해"
Reading additional input from stdin...
Not inside a trusted directory and --skip-git-repo-check was not specified.
[exit=1]

플래그를 붙이면 되는데, 이번엔 출력이 시끄럽다. 헤더(모델·샌드박스·세션 ID)와 진행 로그는 stderr, 최종 답은 stdout으로 나온다. 나뉘어 있으므로 stderr만 버리면 답만 남는다.

$ codex exec --skip-git-repo-check "hello.py가 무엇을 하는지 한 문장으로 설명해" 2>/dev/null
명령줄로 받은 이름 앞에 `"Hello, "`를 붙여 출력하는 파이썬 스크립트입니다.

3.3 Kiro: --no-interactive

기본 에이전트가 읽기 도구(glob/read)를 승인 없이 쓰는데, 그 도구 호출 과정이 stdout에 답과 함께 찍힌다. 끝에는 크레딧과 시간이 표시된다. 이 글의 kiro 출력 인용에서는 도구 로그의 성공 확인 줄과 소요 시간 줄을 ...로 줄인다 — 나머지는 원문 그대로다.

$ kiro-cli chat --no-interactive "hello.py가 무엇을 하는지 한 문장으로 설명해"
Searching for files: **/hello.py (using tool: glob)
Reading file: /path/to/lab/kiro/hello.py, all lines (using tool: read)
...

> 커맨드라인 인자로 받은 이름에 "Hello, "를 붙여 출력하는 스크립트입니다.

 ▸ Credits: 0.29 • Time: 8s

4. 모델과 에포트

좌석 등급과 운항 속도다. 어떤 기체에 태울지(모델), 얼마나 공들여 날지(에포트)를 고른다.

4.1 Claude Code

--model에 별칭(sonnet/opus/haiku)이나 전체 모델 ID, --effort에 low/medium/high를 준다.

$ claude -p "hello.py가 무엇을 하는지 한 문장으로 설명해" --model sonnet --effort low < /dev/null
커맨드라인 인자로 받은 이름을 이용해 "Hello, {name}" 문자열을 출력하는 스크립트입니다.
$ claude -p "hello.py가 무엇을 하는지 한 문장으로 설명해" --model sonnet --effort high < /dev/null
커맨드라인 인자로 받은 이름에 "Hello, "를 붙여 출력하는 간단한 인사 스크립트입니다.

4.2 Codex

모델은 -m. 에포트는 전용 플래그가 없고 -c model_reasoning_effort=...로 설정값을 덮어쓴다. 값이 TOML이라 문자열에 따옴표가 한 겹 더 필요하다 — 이걸 몰라서 한 번 헤맸다.

$ codex exec --skip-git-repo-check -m gpt-5.6-sol -c model_reasoning_effort='"low"' "hello.py가 무엇을 하는지 한 문장으로 설명해" 2>/dev/null

low의 답은 3.2와 글자까지 같았다. high만 표현이 조금 달라진다.

$ codex exec --skip-git-repo-check -m gpt-5.6-sol -c model_reasoning_effort='"high"' "hello.py가 무엇을 하는지 한 문장으로 설명해" 2>/dev/null
명령줄 인자로 받은 이름 앞에 `"Hello, "`를 붙여 출력하는 프로그램입니다.

4.3 Kiro

--model에 2장 --list-models의 이름을 그대로 쓰고, --effort는 low/medium/high/xhigh/max까지 받는다.

$ kiro-cli chat --no-interactive --model claude-sonnet-5 --effort low "hello.py가 무엇을 하는지 한 문장으로 설명해"
Searching for files: **/hello.py (using tool: glob)
Reading file: /path/to/lab/kiro/hello.py, all lines (using tool: read)
...

> hello.py는 커맨드라인 인자로 받은 이름을 받아 "Hello, {name}" 형태의 인사말을 출력하는 스크립트입니다.

 ▸ Credits: 0.17 • Time: 8s
$ kiro-cli chat --no-interactive --model claude-sonnet-5 --effort high "hello.py가 무엇을 하는지 한 문장으로 설명해"
...

> hello.py는 커맨드라인 인자로 받은 이름을 사용해 "Hello, 이름" 형식의 인사말을 출력하는 스크립트입니다.

 ▸ Credits: 0.16 • Time: 7s

운임 차이는 실제로 체감된다. 같은 질문("함수 개수만 숫자로")이 기본 모델에서 0.26크레딧, haiku 계열에서 0.03크레딧이었다.

세 CLI 모두 에포트 low/high의 답 차이는 이 단순한 파일에서는 미미했다. 표현이 조금 달라지는 정도다.

5. 에이전트

에이전트는 직무 규정이 정해진 전담 승무원이다. 응대 방식(시스템 프롬프트)과 출입 가능 구역(도구 제한)을 파일로 정의해 두고 불러 쓴다. 여기서 3사의 구조 차이가 가장 크다.

세 항공사의 전담 승무원

그림: 항공사마다 다른 방식으로 브리핑받는 전담 승무원. AI(Gemini)로 생성한 그림입니다.

5.1 Claude Code: --agent + 프론트매터

프로젝트의 .claude/agents/<name>.md에 정의를 두고 --agent <name>으로 부른다. 프론트매터가 시스템 프롬프트 아래에 쓸 도구(tools)와 모델(model)을 정하고, 본문이 시스템 프롬프트다.

.claude/agents/reviewer.md:

---
name: reviewer
description: Reviews Python files and reports problems one per line
tools: Read
model: sonnet
---
You are a code reviewer. Read the file the user names.
Report each problem on its own line as `file:line: problem`.
Output nothing else: no greeting, no summary, no verdict.
$ claude -p "hello.py를 리뷰해" --agent reviewer < /dev/null
hello.py:2: name이 None이거나 문자열이 아닐 경우 TypeError 발생 가능, 타입 검증 없음
hello.py:7: sys.argv[1] 접근 시 인자를 전달하지 않으면 IndexError 발생, 인자 개수 검증 없음

tools를 좁힐수록 에이전트가 스스로 알아낼 수 있는 것이 줄어드니, 경로 같은 컨텍스트는 프롬프트에 명확히 주는 편이 안전하다.

정의 파일 없이 --agents에 JSON을 인라인으로 넘길 수도 있다. 일회성 파이프라인에 맞는 형태다.

$ claude -p "./hello.py 를 리뷰해" --agents '{"reviewer2":{"description":"Reviews Python files","prompt":"Report each problem as file:line: problem. Nothing else.","tools":["Read"],"model":"sonnet"}}' --agent reviewer2 < /dev/null
hello.py:7: `sys.argv[1]`을 인덱스 체크 없이 사용 — 인자 없이 실행하면 `IndexError` 발생.
hello.py:2: `name`이 문자열이 아닐 경우(int 등) `+` 연산에서 `TypeError` 발생 — 타입 검증/캐스팅 없음.
hello.py:1: `greet` 함수에 타입 힌트와 독스트링 없음.

5.2 Codex (고급): 플래그가 없다, 프롬프트로 spawn

Codex의 에이전트는 메인 세션이 띄우는 서브에이전트다. Claude의 --agent처럼 "이 에이전트로 실행"하는 플래그는 없고, 프롬프트에서 spawn을 지시한다. 개념이 다르므로 처음이라면 건너뛰어도 된다.

.codex/agents/reviewer.toml:

name = "reviewer"
description = "Reviews Python files and reports problems one per line"
developer_instructions = """
You are a code reviewer. Read the file you are asked about.
Report each problem on its own line as `file:line: problem`.
Output nothing else: no greeting, no summary, no verdict.
"""
$ codex exec --skip-git-repo-check "reviewer 서브에이전트를 spawn 해서 hello.py 를 리뷰시키고, 그 결과만 그대로 출력해"
...
user
reviewer 서브에이전트를 spawn 해서 hello.py 를 리뷰시키고, 그 결과만 그대로 출력해
codex
`reviewer` 서브에이전트에게 `hello.py` 코드 리뷰를 맡기겠습니다. 완료되면 결과만 그대로 전달하겠습니다.
collab: Wait
codex
- 보통 — [hello.py:7](/path/to/lab/codex/hello.py:7): 명령줄 인자 없이 실행하면 `sys.argv[1]` 접근으로 `IndexError`가 발생합니다. 인자 개수를 확인하고 사용법을 출력하거나 기본값을 처리해야 합니다.
...

진행 로그(stderr)에 collab: Wait가 찍혀 실제로 spawn됐음을 확인할 수 있고, 리뷰 결과가 돌아왔다. 줄인 끝부분에는 3.2처럼 stdout의 최종 답이 한 번 더 찍힌다 — stderr를 버리면 그 한 줄만 남는다.

5.3 Kiro: tools와 allowedTools의 구분

.kiro/agents/reviewer.json:

{
  "name": "reviewer",
  "description": "Reviews Python files and reports problems one per line",
  "prompt": "You are a code reviewer. Read the file the user names. Report each problem on its own line as `file:line: problem`. Output nothing else: no greeting, no summary, no verdict.",
  "tools": ["read"],
  "allowedTools": ["read"]
}

tools가 쓸 수 있는 도구, allowedTools가 승인 없이 쓰는 도구다. 호출은 Claude와 같은 --agent.

$ kiro-cli chat --no-interactive --agent reviewer "hello.py를 리뷰해"
Reading file: /path/to/lab/kiro/hello.py, all lines (using tool: read)
...

> hello.py:1: greet has no type hints and no docstring
hello.py:2: string concatenation fails with TypeError on non-str name; use an f-string
hello.py:6: import sys placed inside the block instead of at module top (PEP 8)
hello.py:7: sys.argv[1] unguarded — IndexError when run with no argument
hello.py:7: no main() function or argument parsing; logic is inline in the __main__ block
hello.py:7: exit code is always 0 even on missing input; no sys.exit on error

 ▸ Credits: 0.16 • Time: 0s

같은 프롬프트인데 6줄 — 세 CLI 중 가장 상세한 리뷰가 나왔다. 한국어로 물었는데 답이 영어로 나온 점도 눈에 띈다. 에이전트 프롬프트(영어)를 따라간 것으로 보이는데, 답의 언어까지 고정하고 싶으면 그것도 프롬프트에 적어야 한다.

6. 에이전트 + 스킬

스킬은 표준 절차 카드다. 어느 승무원이 펼쳐도 같은 절차가 나오는 재사용 지시문 묶음으로, 세 CLI 모두 SKILL.md라는 같은 형식을 쓰는데 두는 위치와 부르는 방법이 다르다. 실험에는 세 CLI에 같은 내용의 lab-check 스킬을 만들었다.

---
name: lab-check
description: Inspect a Python file and answer in the fixed lab format
---
Read the file the user names. Reply in exactly this format and nothing else:

FILE: <file name>
FUNCTIONS: <number of top-level functions>
RISK: <one line, the single biggest runtime risk>

6.1 Claude Code: /<name>과 $ARGUMENTS, 에이전트에 싣기

위치는 .claude/skills/lab-check/SKILL.md. 프롬프트에서 /lab-check hello.py로 부르면 $ARGUMENTS에 뒤의 글이 들어간다.

$ claude -p "/lab-check hello.py" < /dev/null
FILE: hello.py
FUNCTIONS: 1
RISK: `sys.argv[1]` raises IndexError when run with no command-line argument.

에이전트 프론트매터에 skills: lab-check로 스킬을 미리 실어 두면, 사용자는 스킬 이름을 몰라도 된다. .claude/agents/checker.md:

---
name: checker
description: Checks Python files using the lab-check skill
tools: Read, Skill
skills: lab-check
model: sonnet
---
For any file the user names, use the lab-check skill and return its output unchanged.
$ claude -p "hello.py 점검해" --agent checker < /dev/null
FILE: hello.py
FUNCTIONS: 1
RISK: IndexError if the script is run without a command-line argument (sys.argv[1])

이 패턴은 8.4에서 다시 쓴다.

6.2 Codex: $<name> 또는 자연어

위치는 .agents/skills/lab-check/SKILL.md. 프롬프트에서 $lab-check hello.py로 부른다.

$ codex exec --skip-git-repo-check "\$lab-check hello.py" 2>/dev/null
FILE: hello.py
FUNCTIONS: 1
RISK: Accessing sys.argv[1] raises IndexError when no argument is provided.

"lab-check 스킬로 hello.py 를 점검해"처럼 자연어로 말해도 된다.

$ codex exec --skip-git-repo-check "lab-check 스킬로 hello.py 를 점검해" 2>/dev/null
FILE: hello.py
FUNCTIONS: 1
RISK: 명령줄 인수가 없으면 sys.argv[1]에서 IndexError가 발생함

6.3 Kiro: 자동 로드

위치는 .kiro/skills/lab-check/SKILL.md. 기본 에이전트가 자동으로 읽어들이므로 프롬프트에서 이름만 말하면 된다.

$ kiro-cli chat --no-interactive "lab-check 스킬로 hello.py 를 점검해"
> I'll read the skill definition first, then the target file.
Batch fs_read operation with 2 operations (using tool: read)

↱ Operation 1: Reading file: /path/to/lab/kiro/.kiro/skills/lab-check/SKILL.md, all lines
↱ Operation 2: Reading file: /path/to/lab/kiro/hello.py, all lines
...

> FILE: hello.py
FUNCTIONS: 1
RISK: sys.argv[1] raises IndexError when no command-line argument is supplied.

 ▸ Credits: 0.23 • Time: 0s

에이전트 정의의 resources에 skill://로 스킬을 실어 두면 사용자는 스킬 이름을 몰라도 된다. .kiro/agents/checker.json:

{
  "name": "checker",
  "description": "Checks Python files using the lab-check skill",
  "prompt": "For any file the user names, use the lab-check skill and return its output unchanged.",
  "tools": ["read"],
  "allowedTools": ["read"],
  "resources": ["skill://.kiro/skills/**/SKILL.md"]
}
$ kiro-cli chat --no-interactive --agent checker "hello.py 점검해"
Batch fs_read operation with 2 operations (using tool: read)
Purpose: Read lab-check skill and list working directory

↱ Operation 1: Reading file: /path/to/lab/kiro/.kiro/skills/lab-check/SKILL.md, all lines
↱ Operation 2: Reading directory: /path/to/lab/kiro
...

Reading file: /path/to/lab/kiro/hello.py, all lines (using tool: read)
...

> FILE: hello.py
FUNCTIONS: 1
RISK: sys.argv[1] raises IndexError when run without a command-line argument.

 ▸ Credits: 0.24 • Time: 2s

6.4 스킬을 언급하지 않으면?

확인용 실험 하나. 스킬을 언급하지 않은 요청("hello.py의 함수 개수만 숫자로")에 세 CLI가 어떻게 반응하는지 봤다. Claude와 Codex는 1만 반환했지만 Kiro는 —

$ kiro-cli chat --no-interactive "hello.py의 함수 개수만 숫자로"
Batch fs_read operation with 2 operations (using tool: read)

↱ Operation 1: Reading file: /path/to/lab/kiro/.kiro/skills/lab-check/SKILL.md, all lines
↱ Operation 2: Reading file: /path/to/lab/kiro/hello.py, all lines
...

> FILE: hello.py
FUNCTIONS: 1
RISK: sys.argv[1] raises IndexError when no argument is passed.

 ▸ Credits: 0.22 • Time: 6s

SKILL.md까지 읽어 들여 FILE/FUNCTIONS/RISK 포맷으로 답했다.

참고 — 발화가 스킬을 부른다. 오동작이 아니다. AI 에이전트에서 스킬과 에이전트는 명시적 호출뿐 아니라 발화와의 관련성으로도 트리거되는 게 설계 특성이고, 이번 실행에서는 Kiro의 기본 에이전트가 그 트리거에 가장 적극적이었을 뿐이다. 관련돼 보이는 절차 카드를 알아서 꺼내 드는 승무원인 셈이다. 뒤집어 말하면 프로젝트에 어떤 스킬을 두는지가 곧 답의 형태를 좌우한다 — 컨텍스트와 스킬·에이전트 관리가 중요한 이유다. 이후 옵션 실험은 변인을 통제하려고 스킬을 지운 뒤 진행했다.

7. 자주 쓰는 옵션

보안검색대 구간이다. headless에는 승인 창, 즉 현장 검사관이 없다. 무엇을 반입할지(도구 허용)는 미리 신고해야 하고, 신고 안 한 반입품은 그 자리에서 거부된다.

7.1 Claude Code: JSON 출력, 도구 허용, 폴더 밖

--output-format json은 답과 함께 비용·세션 정보를 준다.

$ claude -p "hello.py의 함수 개수만 숫자로" --output-format json < /dev/null | jq "{total_cost_usd, duration_ms, num_turns}"
{
  "total_cost_usd": 0.06357600000000001,
  "duration_ms": 10843,
  "num_turns": 4
}

답만 필요하면 jq -r .result.

$ claude -p "hello.py의 함수 개수만 숫자로" --output-format json < /dev/null | jq -r .result
1

이벤트 단위(도구 호출 포함)가 필요하면 --output-format stream-json --verbose로 받는다. 에이전트가 어떤 도구를 어떤 인자로 불렀는지 확인할 때 유용하다.

도구 허용이 headless의 핵심 차이다. 허용하지 않은 도구(Bash)를 쓰려 하면 그냥 거부하고 끝난다.

$ claude -p "python3 hello.py World 를 실행하고 출력만 알려줘" < /dev/null
 명령 실행이 승인되지 않아 `python3 hello.py World`를 실행하지 못했습니다. ...

--allowedTools "Bash(python3 *)"로 미리 허용하면 실행된다. 패턴이므로 python3로 시작하는 명령만 열린다.

$ claude -p "python3 hello.py World 를 실행하고 출력만 알려줘" --allowedTools "Bash(python3 *)" < /dev/null

Hello, World


--add-dir은 현재 폴더 밖의 파일도 읽게 한다.

$ claude -p "../other/hello.py 와 hello.py 가 같은 파일인지 한 줄로" --add-dir ../other < /dev/null
내용은 동일하지만(md5 `18a0a027…`, 119바이트) inode가 달라(2058704 vs 2045973) 서로 다른 파일의 사본입니다.

--json-schema는 답을 정해진 JSON 형태로 받는다(--output-format json과 함께).

$ claude -p "hello.py를 분석해" --output-format json --json-schema '{"type":"object","properties":{"functions":{"type":"integer"},"has_argv_check":{"type":"boolean"}},"required":["functions","has_argv_check"]}' < /dev/null | jq .structured_output
{
  "functions": 1,
  "has_argv_check": false
}

7.2 Codex: 샌드박스, 파일 출력, 스키마

--json은 이벤트를 JSON Lines로 stdout에 낸다. 마지막 agent_message만 jq로 고른다.

$ codex exec --skip-git-repo-check --json "hello.py의 함수 개수만 숫자로" 2>/dev/null | jq -rs "[.[] | select(.type==\"item.completed\" and .item.type==\"agent_message\")] | last | .item.text"
1

-o <file>은 최종 답만 파일로 저장한다.

$ codex exec --skip-git-repo-check -o last.txt "hello.py의 함수 개수만 숫자로" >/dev/null 2>&1; cat last.txt
1

--output-schema는 답을 JSON 스키마에 맞춘다. result-schema.json:

{
  "type": "object",
  "properties": {
    "functions": { "type": "integer" },
    "has_argv_check": { "type": "boolean" }
  },
  "required": ["functions", "has_argv_check"],
  "additionalProperties": false
}
$ codex exec --skip-git-repo-check --output-schema result-schema.json -o last.txt "hello.py를 분석해" >/dev/null 2>&1; cat last.txt
{"functions":1,"has_argv_check":false}

도구 허용은 -s/--sandbox다 — read-only(기본) / workspace-write / danger-full-access. 기본값에서 쓰기 요청은 명확하게 거절된다.

$ codex exec --skip-git-repo-check -s read-only "hello.py 옆에 note.txt 파일을 만들어" 2>/dev/null; ls note.txt
현재 작업공간이 읽기 전용이라 `note.txt`를 만들 수 없습니다. 쓰기 권한을 허용한 뒤 다시 요청해 주세요. `hello.py`는 작업공간 루트에 있습니다.
ls: cannot access 'note.txt': No such file or directory
[exit=2]

-s workspace-write로 바꾸면 같은 요청이 성공한다.

$ codex exec --skip-git-repo-check -s workspace-write "hello.py 옆에 note.txt 파일을 만들어" 2>/dev/null; ls note.txt; rm -f note.txt
`hello.py` 옆에 빈 [note.txt](/path/to/lab/codex/note.txt) 파일을 만들었습니다.
note.txt

--ephemeral은 세션 기록을 남기지 않는다 — 일회성 호출에 적합하다.

$ codex exec --skip-git-repo-check --ephemeral "hello.py의 함수 개수만 숫자로" 2>/dev/null
1

7.3 Kiro: trust, 그리고 응답 ≠ 실행 증거

Kiro 비대화형의 기본값은 명령 실행(shell) 자동 거부다. 그런데 거부하고 멈추는 게 아니라, 파일을 읽고 예상 출력을 추측해서 답한다.

$ kiro-cli chat --no-interactive "python3 hello.py World 를 실행하고 출력만 알려줘"
I will run the following command: python3 hello.py World (using tool: shell)
Purpose: hello.py를 World 인자로 실행

Command execute_bash is rejected because it matches one or more rules on the denied list:
  - non-interactive mode (no user to approve)

Reading file: /path/to/lab/kiro/hello.py, all lines (using tool: read)
...

> 실행은 못 했습니다. 이 환경은 비대화형 모드라 명령 실행 승인을 받을 수 없어서 execute_bash가 차단됐습니다.

대신 hello.py를 읽어보면, sys.argv[1]을 받아 "Hello, " + name을 출력하므로 예상 출력은 다음과 같습니다:

Hello, World


실제 실행 결과가 필요하면 터미널에서 직접 python3 hello.py World를 실행해 주세요.

 ▸ Credits: 0.28 • Time: 10s

이 답은 추측임을 스스로 밝히고 있지만, 8장처럼 답을 중계하는 구조에서는 "실행했더니 Hello, World"와 "실행은 거부됐지만 아마 Hello, World"의 구분이 지워질 수 있다. 결항됐는데 예상 도착 시간을 안내받은 셈이다 — headless 결과는 거부됨 · 실행됨 · 추론됨 세 상태로 읽어야 한다.

결항인데 예상 도착 시간을 받았다

그림: 결항인데 예상 도착 시간을 받았다 — 응답은 실행 증거가 아니다. AI(Gemini)로 생성한 그림입니다.

실제로 실행시키려면 신뢰를 준다. --trust-all-tools는 모든 도구 자동 승인 — 명령 실행까지 허용되므로 신뢰하는 프롬프트에만.

$ kiro-cli chat --no-interactive --trust-all-tools "python3 hello.py World 를 실행하고 출력만 알려줘"
All tools are now trusted (!). Kiro will execute tools without asking for confirmation.
Agents can sometimes do unexpected things so understand the risks.
...

I will run the following command: python3 hello.py World (using tool: shell)
Purpose: hello.py를 World 인자로 실행

Hello, World
 - Completed in 0.16s

> Hello, World

 ▸ Credits: 0.17 • Time: 4s

--trust-tools=<이름들>은 골라서 신뢰한다. 읽기만 열면:

$ kiro-cli chat --no-interactive --trust-tools=fs_read "hello.py의 함수 개수만 숫자로"
Searching for files: **/hello.py (using tool: glob)
Getting symbols from: /path/to/lab/kiro/hello.py (using tool: code)
  1. Function greet at hello.py:1:1
...

> 1

 ▸ Credits: 0.26 • Time: 8s

구조화 출력은 --output-format stream-json인데 --agent-engine v2가 필요하다(v1 엔진은 에러). 최종 답은 runFinished 이벤트에 있다.

$ kiro-cli chat --output-format stream-json --agent-engine v2 "hello.py의 함수 개수만 숫자로" | jq -r "select(.type==\"runFinished\") | .data.finalText"
1

8. 서로 부르게 하기

코드셰어 편이다. 한 항공사의 표로 다른 항공사의 기체를 탄다 — 1.1의 표에서 봤듯 공식 코드셰어 협정은 Codex 플러그인 하나뿐이므로, 나머지 노선은 직접 놓는다. Claude Code의 Bash 도구로 codex exec / kiro-cli chat --no-interactive를 실행시키고 결과만 받는 것이다.

CLAUDE 표로 CODEX 기체에 탑승

그림: CLAUDE 표로 CODEX 기체에 탑승 — 코드셰어. AI(Gemini)로 생성한 그림입니다.

8.1 Claude → Codex

7.1에서 본 대로 headless Claude는 미허용 Bash를 거부한다. 뒤집으면, --allowedTools에 원하는 명령만 열어 주면 된다.

$ claude -p "codex exec --skip-git-repo-check 명령으로 hello.py 리뷰를 시키고, codex 의 최종 답만 그대로 전달해" --allowedTools "Bash(codex exec*)" < /dev/null
 codex의 최종 답변 그대로 전달합니다:

---

### 리뷰 결과

- **중간 — 인자 없이 실행하면 예외가 발생합니다.**
  [hello.py:7](/path/to/lab/claude/hello.py#L7)에서 `sys.argv[1]`을 바로 참조하므로 `python hello.py` 실행 시 `IndexError`가 발생합니다. 인자 개수를 검사하고 사용법을 출력하는 처리가 필요합니다.

- **낮음 — 문자열이 아닌 값에서 `greet()`가 실패합니다.**
  [hello.py:2](/path/to/lab/claude/hello.py#L2)의 문자열 덧셈은 `greet(123)`처럼 호출하면 `TypeError`를 냅니다. 문자열만 허용할 의도라면 타입 힌트를 추가하고, 아니라면 f-string으로 변환하는 편이 안전합니다.

추가로 정상 입력, 빈 문자열, CLI 인자 누락을 검증하는 테스트가 없습니다. 핵심 동작은 단순하고 정상적인 문자열 인자를 전달할 때는 올바르게 작동합니다.

Claude가 codex exec를 실행하고, Codex의 답을 그대로 중계해 왔다. Bash(codex exec*) 패턴이므로 codex exec로 시작하는 명령만 허용되고 나머지 Bash는 여전히 거부된다.

8.2 Claude → Kiro

허용 패턴과 안쪽 명령만 바뀐다.

$ claude -p "kiro-cli chat --no-interactive 명령으로 hello.py 요약을 받아, kiro 의 답만 그대로 전달해" --allowedTools "Bash(kiro-cli chat*)" < /dev/null
kiro의 답변:

> `hello.py`는 7줄짜리 간단한 인사 스크립트입니다.
>
> - `greet(name)`: 문자열 연결로 `"Hello, {name}"`을 반환합니다.
> - `__main__` 블록: `sys`를 함수 밖(블록 안)에서 import하고, `sys.argv[1]`을 받아 결과를 출력합니다.
>
> 참고로 인자 없이 실행하면 `sys.argv[1]`에서 `IndexError`가 발생합니다. 인자 개수 검사나 사용법 안내를 추가하면 좋습니다.

8.3 커스텀 스킬 ask-codex / ask-kiro

매번 저 긴 명령을 치기는 번거롭다. 6장의 스킬로 포장하면 한 단어 — 코드셰어 티켓 한 장이 된다. .claude/skills/ask-codex/SKILL.md:

---
name: ask-codex
description: Ask Codex CLI (headless) and relay its answer unchanged
allowed-tools: Bash(codex exec*)
---
Run exactly this command, with $ARGUMENTS as the prompt:

    codex exec --skip-git-repo-check "$ARGUMENTS" 2>/dev/null

Reply with Codex's final answer only. Do not add your own analysis.

.claude/skills/ask-kiro/SKILL.md는 안쪽 명령만 다르다:

---
name: ask-kiro
description: Ask Kiro CLI (headless) and relay its answer unchanged
allowed-tools: Bash(kiro-cli chat*)
---
Run exactly this command, with $ARGUMENTS as the prompt:

    kiro-cli chat --no-interactive "$ARGUMENTS"

Reply with Kiro's final answer only. Do not add your own analysis.

이 파일 하나에 이 글의 재료가 다 들어 있다. 안쪽 명령은 3.2의 headless 호출과 stderr 처리, frontmatter의 allowed-tools는 7.1의 --allowedTools를 대신하고(그래서 커맨드라인이 짧아진다), $ARGUMENTS는 6.1의 스킬 문법이다. 실행 결과는 1.1에서 이미 봤다 — /ask-codex는 세 줄 리뷰, /ask-kiro는 한 문장 요약을 그대로 중계했다.

경계는 분명히 해 두고 싶다. "relay unchanged"는 파이프 같은 기계적 보장이 아니라 상위 모델에 대한 지시문이다. Claude가 요약하거나 덧붙일 가능성이 원리적으로 남아 있고, 이번 실행들에서 그대로 중계된 것을 확인했을 뿐이다. 그리고 7.3의 세 상태 구분 — 중계된 답이 "실행됨"인지 "추론됨"인지는 원 출력을 봐야 안다.

8.4 제안: 스킬과 에이전트로 확장

여기서부터는 검증한 사실이 아니라 조합 제안이다.

첫째, 두 스킬을 ~/.claude/skills/에 두면 어느 프로젝트에서든 /ask-codex, /ask-kiro가 된다. 다만 6.4에서 본 발화 기반 자동 트리거의 적용 범위도 같이 넓어진다.

둘째, 4장의 모델·에포트 플래그를 스킬 안 명령에 하드코딩하면 용도별 스킬이 된다. 싼 모델(luna 0.10x, haiku 0.40x)로 요약 스킬을, 비싼 모델로 리뷰 스킬을 만드는 식이다.

셋째, 에이전트와 조합할 수 있다. 6.1의 checker 패턴 — 프론트매터 skills:에 스킬을 실은 에이전트 — 에 ask-codex를 실으면, 스킬 이름을 모르는 채로 "이 파일 교차 리뷰해"만 해도 Codex의 시선이 들어오는 에이전트가 된다. 이 조합 자체는 아직 돌려 보지 않았다.

9. 마치며: 주의, 그리고 숙제

실행에서 확인된 주의점을 모아 둔다. 전부 기록에 증거가 있다.

  • Claude 에이전트의 tools를 좁힐 땐 경로 같은 컨텍스트를 프롬프트에 명확히 준다. 좁힌 만큼 에이전트가 스스로 알아낼 수단이 줄어든다.
  • Claude headless는 승인 창이 없어 Bash 등은 --allowedTools 없으면 거부.
  • Codex는 git 저장소 밖에서 --skip-git-repo-check 필수. 진행 로그는 stderr, 답은 stdout.
  • Codex 기본 샌드박스 read-only에서 파일 쓰기 요청은 거절(exit 2).
  • 도구 호출이 거부되면 모델이 추측으로 빈자리를 메워 답할 수 있다 — 이번엔 Kiro의 shell 거부에서 관찰됐지만 특정 CLI가 아니라 LLM 일반의 특성이다. 응답 ≠ 실행 증거, 거부됨·실행됨·추론됨을 구분해 읽는다.
  • Kiro --output-format stream-json은 --agent-engine v2 필요. 최종 답은 runFinished.data.finalText.
  • 스킬을 프로젝트에 두면 언급하지 않은 요청에도 발화 관련성으로 트리거될 수 있다(Kiro에서 재현, Codex·Claude는 이번 실행에서 미재현). 컨텍스트와 스킬·에이전트 관리가 중요한 이유다.

해 보지 않은 것은 숙제로 남긴다. 전역 스킬 배치(~/.claude/skills/), 에이전트에 ask-codex를 싣는 조합(8.4), 그리고 반대 방향 — Codex나 Kiro 안에서 claude -p를 부르는 것. 재료는 이 글에 다 있다. 반대 방향이 특히 좋은 연습이다: 3장의 호출 한 줄과 7장의 도구 허용만 조합하면 되는데, 각 CLI의 스킬 체계가 그 조합을 얼마나 짧게 만들어 주는지 확인해 보시라.

결국 세 CLI의 headless는 같은 비행 절차다. 한 항공사를 알면 나머지는 안내판 번역 문제고, 그 대조표가 1.2의 대응표다. 경유(일회성 호출)로 답만 받아 나오고, 좌석 등급(모델·에포트)을 고르고, 전담 승무원(에이전트)과 절차 카드(스킬)를 실어 두고, 반입품(도구 허용)은 보안검색 전에 미리 신고한다. 그 위에 코드셰어(크로스 호출 스킬)를 얹으면 한 항공사의 표로 세 항공사의 기체를 탈 수 있다. 어디까지 노선을 늘려야 충분한지는 아직 잘 모르겠다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다