비대화형 환경 (Non-Interactive Environment)
관련 소스 파일
이 문서는 사용자 입력으로 인해 bash 명령어가 대기 상태(hang)에 빠지는 것을 방지하기 위해 환경 변수를 자동으로 주입하고 금지된 대화형 명령어를 감지하는 비대화형 환경 훅(non-interactive environment hook)에 대해 설명합니다. 이러한 안정성 조치는 사람이 대화형 프롬프트에 응답할 수 없는 CI/CD 환경 및 자동화된 에이전트 실행에 있어 매우 중요합니다.
더 광범위한 안정성 아키텍처에 대해서는 안정성 시스템(Reliability System)을 참조하십시오. 기타 bash 관련 훅에 대해서는 기타 훅(Other Hooks)을 참조하십시오.
목적 및 범위 (Purpose and Scope)
비대화형 환경 훅(createNonInteractiveEnvHook)은 모든 bash 도구 실행을 가로채고 환경을 수정하여 명령어가 사용자 입력을 기다리지 않도록 합니다. 이는 oh-my-opencode가 다음과 같은 환경에서 실행될 때 필수적입니다:
- GitHub Actions 워크플로우 (Sisyphus Agent)
OPENCODE_RUN=true설정이 된 CLIrun명령어CI=true설정이 된 모든 CI/CD 환경- Non-TTY 환경 (파이프 출력, 헤드리스 실행)
이 훅이 없으면 -m 옵션이 없는 git commit, -y 옵션이 없는 apt-get install, 또는 대화형 에디터(vim, nano)와 같은 명령어들이 무기한 대기 상태에 빠져 전체 세션이 중단될 수 있습니다.
출처: src/hooks/non-interactive-env/index.ts L1-L53
src/hooks/non-interactive-env/detector.ts L1-L19
아키텍처 개요 (Architecture Overview)
이 훅은 tool.execute.before 단계에서 작동하며, bash 명령어가 OpenCode SDK에 도달하기 전에 가로챕니다. 실행 환경을 수정하고, 문제가 될 수 있는 명령어에 대해 선택적으로 경고 메시지를 추가합니다.
훅 실행 흐름
flowchart TD
AgentCall["에이전트가 bash 도구 호출<br>명령어, 환경 변수"]
HookTrigger["tool.execute.before 이벤트<br>입력: tool, sessionID, callID<br>출력: args, message"]
ToolCheck["tool === 'bash'?"]
InjectEnv["NON_INTERACTIVE_ENV 주입<br>11개의 환경 변수"]
DetectBanned["detectBannedCommand()<br>정규식 패턴 매칭"]
HasBanned["금지된 명령어<br>감지됨?"]
AddWarning["output.message 설정<br>⚠️ 경고: ... 대기 상태에 빠질 수 있음"]
Execute["수정된 환경으로 bash 실행<br>OpenCode SDK"]
AgentCall -.-> HookTrigger
HookTrigger -.-> ToolCheck
ToolCheck -.->|"아니오"| Execute
ToolCheck -.->|"예"| InjectEnv
InjectEnv -.-> DetectBanned
DetectBanned -.-> HasBanned
HasBanned -.->|"아니오"| Execute
HasBanned -.->|"예"| AddWarning
AddWarning -.-> Execute
출처: src/hooks/non-interactive-env/index.ts L22-L53
환경 변수 주입 (Environment Variable Injection)
이 훅은 모든 bash 실행에 11개의 환경 변수를 주입하여, 일반적인 도구와 패키지 관리자가 비대화형으로 작동하도록 보장합니다.
NON_INTERACTIVE_ENV 변수
| 변수 | 값 | 목적 |
|---|---|---|
CI |
"true" |
범용 CI 표시기 (npm, pip, git이 이를 감지함) |
DEBIAN_FRONTEND |
"noninteractive" |
apt-get/dpkg 대화형 프롬프트 방지 |
GIT_TERMINAL_PROMPT |
"0" |
git 자격 증명 프롬프트 비활성화 |
GCM_INTERACTIVE |
"never" |
Git Credential Manager 비대화형 모드 |
HOMEBREW_NO_AUTO_UPDATE |
"1" |
Homebrew 자동 업데이트 프롬프트 건너뛰기 |
GIT_EDITOR |
"true" |
git rebase/commit 에디터 차단 (true = 아무 작업도 하지 않음) |
EDITOR |
"true" |
시스템 에디터 호출 차단 |
VISUAL |
"true" |
비주얼 에디터 폴백 차단 |
GIT_SEQUENCE_EDITOR |
"true" |
git rebase -i 에디터 차단 |
GIT_PAGER |
"cat" |
less/more를 cat으로 대체 (페이지 매김 없음) |
PAGER |
"cat" |
시스템 페이저(pager) 대체 |
npm_config_yes |
"true" |
npm 자동 확인 프롬프트 |
PIP_NO_INPUT |
"1" |
Python pip 비대화형 모드 |
YARN_ENABLE_IMMUTABLE_INSTALLS |
"false" |
lockfile 없이 yarn 진행 허용 |
구현:
// 실행 전 환경 변수 병합
output.args.env = {
...(output.args.env as Record<string, string> | undefined),
...NON_INTERACTIVE_ENV,
}
이는 에이전트가 명시적으로 설정한 환경 변수를 보존하면서 비대화형 기본값을 추가합니다.
출처: src/hooks/non-interactive-env/constants.ts L3-L23
src/hooks/non-interactive-env/index.ts L37-L40
금지된 명령어 감지 (Banned Command Detection)
이 훅은 항상 대기 상태에 빠지는 대화형 명령어를 감지하고 도구 실행에 경고 메시지를 추가합니다. 에이전트는 경고를 보게 되지만 명령어는 여전히 실행됩니다 (에이전트가 실패로부터 학습할 수 있도록 함).
금지된 명령어
금지된 명령어 카테고리
flowchart TD
gitaddp["git add -p"]
gitrebasei["git rebase -i"]
python["python (no -c/-e)"]
node["node (no -e)"]
vim["vim"]
nano["nano"]
vi["vi"]
emacs["emacs"]
less["less"]
more["more"]
man["man"]
BannedPattern["BANNED_COMMAND_PATTERNS<br>정규식: \bcommand\b"]
vim -.-> BannedPattern
nano -.-> BannedPattern
vi -.-> BannedPattern
emacs -.-> BannedPattern
less -.-> BannedPattern
more -.-> BannedPattern
man -.-> BannedPattern
subgraph subGraph1 ["페이저 (항상 차단)"]
less
more
man
end
subgraph subGraph0 ["에디터 (항상 차단)"]
vim
nano
vi
emacs
end
subgraph subGraph3 ["대화형 Git"]
gitaddp
gitrebasei
end
subgraph subGraph2 ["REPL (플래그 없는 경우)"]
python
node
end
감지 로직:
// 명령어 목록에서 정규식 패턴 생성
const BANNED_COMMAND_PATTERNS = SHELL_COMMAND_PATTERNS.banned
.filter((cmd) => !cmd.includes("("))
.map((cmd) => new RegExp(`\\b${cmd}\\b`))
function detectBannedCommand(command: string): string | undefined {
for (let i = 0; i < BANNED_COMMAND_PATTERNS.length; i++) {
if (BANNED_COMMAND_PATTERNS[i].test(command)) {
return SHELL_COMMAND_PATTERNS.banned[i]
}
}
return undefined
}
감지되면 훅은 다음을 추가합니다: ⚠️ Warning: 'vim' is an interactive command that may hang in non-interactive environments.
출처: src/hooks/non-interactive-env/constants.ts L54-L59
src/hooks/non-interactive-env/index.ts L9-L20
src/hooks/non-interactive-env/index.ts L42-L45
쉘 명령어 패턴 (Shell Command Patterns)
상수(constants) 파일은 일반적인 도구에 대한 좋은 패턴과 나쁜 패턴을 기록하여 에이전트와 개발자를 위한 참조 역할을 합니다.
패키지 관리자 패턴
| 도구 | 나쁜 패턴 (Bad Pattern) | 좋은 패턴 (Good Pattern) |
|---|---|---|
| npm | npm init |
npm init -y |
| npm | npm install (프롬프트 포함) |
npm install --yes |
| apt | apt-get install pkg |
apt-get install -y pkg |
| apt | - | DEBIAN_FRONTEND=noninteractive apt-get install pkg |
| pip | pip install pkg (프롬프트 포함) |
pip install --no-input pkg |
| pip | - | PIP_NO_INPUT=1 pip install pkg |
Git 작업 패턴
| 나쁜 패턴 (Bad Pattern) | 좋은 패턴 (Good Pattern) |
|---|---|
git commit |
git commit -m 'msg' |
git merge branch |
git merge --no-edit branch |
git add -p |
git add . |
git rebase -i |
git rebase --no-edit |
시스템 명령어 패턴
| 나쁜 패턴 (Bad Pattern) | 좋은 패턴 (Good Pattern) |
|---|---|
rm file (프롬프트 발생) |
rm -f file |
cp a b (프롬프트 발생) |
cp -f a b |
ssh host |
ssh -o BatchMode=yes host |
unzip file.zip (프롬프트 발생) |
unzip -o file.zip |
입력이 필요한 스크립트에 대한 해결 방법
스크립트가 반드시 대화형 입력을 요구하는 경우, 에이전트는 다음과 같은 기술을 사용할 수 있습니다:
스크립트에 yes 파이프 연결:
yes | ./script.sh
여러 입력을 위한 Heredoc 사용:
./script.sh <<EOF
option1
option2
EOF
설정 파일 선호:
expect나 유사한 도구를 사용하는 대신, 에이전트는 설정 파일 옵션이나 환경 변수 대안을 찾아야 합니다.
출처: src/hooks/non-interactive-env/constants.ts L29-L69
비대화형 감지 (Non-Interactive Detection)
시스템은 여러 환경 지표를 확인하는 isNonInteractive()를 사용하여 비대화형 모드 적용 여부를 결정합니다.
비대화형 감지 결정 트리
flowchart TD
Start["isNonInteractive()"]
CheckCI["CI === 'true'<br>또는 CI === '1'?"]
CheckOpenCode["OPENCODE_RUN === 'true' 또는<br>OPENCODE_NON_INTERACTIVE?"]
CheckGitHub["GITHUB_ACTIONS<br>=== 'true'?"]
CheckTTY["process.stdout.isTTY<br>!== true?"]
ReturnTrue["true 반환<br>비대화형 모드 적용"]
ReturnFalse["false 반환<br>대화형 모드 허용"]
Start -.-> CheckCI
CheckCI -.->|"예"| ReturnTrue
CheckCI -.->|"아니오"| CheckOpenCode
CheckOpenCode -.->|"예"| ReturnTrue
CheckOpenCode -.->|"아니오"| CheckGitHub
CheckGitHub -.->|"예"| ReturnTrue
CheckGitHub -.->|"아니오"| CheckTTY
CheckTTY -.->|"예"| ReturnTrue
CheckTTY -.->|"아니오"| ReturnFalse
주요 감지 방법:
- CI 환경: 표준
CI변수 (Jenkins, Travis, CircleCI 등) - OpenCode 실행 모드: CLI
run명령어는OPENCODE_RUN=true를 설정함 - GitHub Actions: 명시적인
GITHUB_ACTIONS=true확인 - TTY 감지: stdout이 터미널이 아닌 경우 비대화형으로 간주
이러한 다층적 감지는 로컬 대화형 개발 중에는 비활성 상태를 유지하면서 모든 CI/CD 컨텍스트에서 훅이 활성화되도록 보장합니다.
출처: src/hooks/non-interactive-env/detector.ts L1-L19
훅 시스템과의 통합 (Integration with Hook System)
비대화형 환경 훅은 안정성 계층의 일부로 플러그인의 훅 레지스트리에 통합됩니다.
훅 등록
flowchart TD
Plugin["OhMyOpenCodePlugin.initialize()"]
CreateHook["createNonInteractiveEnvHook(ctx)"]
HookObj["{ 'tool.execute.before': handler }"]
Register["ctx.client.events.listen()"]
Plugin -.-> CreateHook
CreateHook -.-> HookObj
HookObj -.-> Register
이 훅은 단일 핸들러를 반환합니다:
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown>; message?: string }
): Promise<void> => {
// 구현 내용
}
}
}
주요 특징:
- 조기 가로채기:
tool.execute.before는 OpenCode SDK가 도구를 실행하기 전에 실행됩니다. - 변이(Mutation) 허용: 훅이
output.args.env를 직접 수정합니다. - 경고 주입: 훅이
output.message를 설정하여 에이전트에게 경고를 표시할 수 있습니다. - 도구 특정적:
bash도구에 대해서만 활성화됩니다.
출처: src/hooks/non-interactive-env/index.ts L22-L53
설정 (Configuration)
이 훅은 oh-my-opencode.json 설정 파일을 통한 선택적 구성을 지원합니다:
interface NonInteractiveEnvConfig {
disabled?: boolean
}
disabled: true인 경우 훅이 등록되지 않습니다. 이는 주로 대화형 명령어가 의도적으로 필요한 디버깅 시나리오를 위한 것입니다.
기본 동작: 설정에서 명시적으로 비활성화하지 않는 한 항상 활성화됩니다.
출처: src/hooks/non-interactive-env/types.ts L1-L3
백그라운드 작업과의 상호작용 (Interaction with Background Tasks)
비대화형 환경 훅은 background_task 도구를 통해 생성된 백그라운드 작업과 원활하게 작동합니다. 각 백그라운드 작업은 자체 bash 실행을 가진 별도의 세션이며, 훅은 이들 모두에 적용됩니다:
- 메인 세션 bash: 비대화형 환경 적용
- 백그라운드 작업 bash: 비대화형 환경 적용
- Sisyphus 에이전트 (GitHub Actions): 비대화형 환경 적용
이를 통해 모든 실행 컨텍스트에서 일관된 동작을 보장하고, 어떤 세션도 사용자 입력으로 인해 대기 상태에 빠지지 않도록 합니다.
출처: src/hooks/non-interactive-env/index.ts L1-L53
사용 예시 (Usage Examples)
예제 1: Git 작업
훅이 없는 경우 (대기 상태 발생):
git commit
# 에디터가 커밋 메시지를 입력받기 위해 대기함
훅이 있는 경우 (성공):
- 에이전트 실행:
git commit - 훅 주입:
GIT_EDITOR=true - 결과: Git이
true를 에디터로 사용(아무 작업 안 함), 에이전트는 메시지 누락에 대한 오류를 확인 - 에이전트 학습 결과:
git commit -m "fix: update config"사용
예제 2: 패키지 설치
훅이 없는 경우 (대기 상태 발생):
apt-get install nginx
# "Do you want to continue? [Y/n]" 프롬프트 발생
훅이 있는 경우 (성공):
- 에이전트 실행:
apt-get install nginx - 훅 주입:
DEBIAN_FRONTEND=noninteractive - 결과: apt-get이 자동 확인하고 설치를 진행함
예제 3: 금지된 명령어 감지
에이전트 시도:
vim config.yaml
훅 응답:
- 경고 메시지:
⚠️ Warning: 'vim' is an interactive command that may hang in non-interactive environments. - 명령어는 여전히 실행됨 (대기 상태에 빠지거나 실패함)
- 에이전트는 실패를 확인하고 다음을 사용하도록 학습함:
echo "key: value" >> config.yaml
출처: src/hooks/non-interactive-env/constants.ts L29-L69
src/hooks/non-interactive-env/index.ts L42-L45
다른 안정성 훅과의 관계 (Relationship to Other Reliability Hooks)
비대화형 환경 훅은 여러 예방 계층(prevention-layer) 훅 중 하나입니다:
- 비대화형 환경 (Non-Interactive Env): bash 대기 상태 방지 (이 페이지)
- 메시지 검증기 (Message Validator) (7.2): API 준수 오류 방지
- 주석 체크 (Comment Checker) (7.7): 코드 내 TODO 주석에 대해 경고
- 세션 복구 (Session Recovery) (7.1): 오류 발생 후 복구
이러한 훅들은 함께 심층 방어(defense-in-depth)를 구현합니다. 예방은 오류를 줄이고, 복구는 예방을 통과한 오류를 처리합니다.