Gemini CLI용 훅 작성하기 (Writing hooks for Gemini CLI)
이 가이드는 간단한 로깅 훅부터 포괄적인 워크플로우 지원까지 Gemini CLI용 훅을 만드는 과정을 안내합니다.
전제 조건 (Prerequisites)
시작하기 전에 다음 사항을 확인하세요.
- Gemini CLI 설치 및 구성 완료
- 쉘 스크립팅 또는 JavaScript/Node.js에 대한 기본적인 이해
- 훅 입력/출력을 위한 JSON에 대한 친숙함
빠른 시작 (Quick start)
기본적인 이해를 돕기 위해 모든 도구 실행을 로깅하는 간단한 훅을 만들어 보겠습니다.
중요한 규칙: 로그는 항상 stderr에 작성하세요. 최종 JSON만 stdout에 작성해야 합니다.
1단계: 훅 스크립트 만들기
훅을 위한 디렉터리와 간단한 로깅 스크립트를 만듭니다.
참고:
이 예제는
jq를 사용하여 JSON을 파싱합니다.jq가 설치되어 있지 않다면 Node.js나 Python을 사용하여 유사한 로직을 수행할 수 있습니다.
mkdir -p .gemini/hooks
cat > .gemini/hooks/log-tools.sh << 'EOF'
#!/usr/bin/env bash
# Read hook input from stdin
input=$(cat)
# Extract tool name (requires jq)
tool_name=$(echo "$input" | jq -r '.tool_name')
# Log to stderr (visible in terminal if hook fails, or captured in logs)
echo "Logging tool: $tool_name" >&2
# Log to file
echo "[$(date)] Tool executed: $tool_name" >> .gemini/tool-log.txt
# Return success (exit 0) with empty JSON
echo "{}"
exit 0
EOF
chmod +x .gemini/hooks/log-tools.sh종료 코드 전략 (Exit Code Strategies)
Gemini CLI에서 작업을 제어하거나 차단하는 두 가지 방법이 있습니다.
| 전략 | 종료 코드 | 구현 | 용도 |
|---|---|---|---|
| 구조적 (관용적) | 0 | {"decision": "deny", "reason": "..."}와 같은 JSON 객체를 반환합니다. | 프로덕션 훅, 사용자 정의 피드백, 복잡한 로직. |
| 비상 정지 | 2 | 에러 메시지를 stderr에 출력하고 종료합니다. | 간단한 보안 게이트, 스크립트 오류, 빠른 프로토타이핑. |
실용적인 예제 (Practical examples)
보안: 커밋 시 비밀 정보 차단 (Security: Block secrets in commits)
API 키나 비밀번호가 포함된 파일을 커밋하지 못하도록 방지합니다. 에이전트에게 구조화된 거부 메시지를 제공하기 위해 종료 코드 0을 사용한다는 점에 유의하세요.
.gemini/hooks/block-secrets.sh:
#!/usr/bin/env bash
input=$(cat)
# Extract content being written
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')
# Check for secrets
if echo "$content" | grep -qE 'api[_-]?key|password|secret'; then
# Log to stderr
echo "Blocked potential secret" >&2
# Return structured denial to stdout
cat <<EOF
{
"decision": "deny",
"reason": "Security Policy: Potential secret detected in content.",
"systemMessage": "🔒 Security scanner blocked operation"
}
EOF
exit 0
fi
# Allow
echo '{"decision": "allow"}'
exit 0동적 컨텍스트 주입 (Git 기록) (Dynamic context injection)
각 에이전트 상호 작용 전에 관련 프로젝트 컨텍스트를 추가합니다.
.gemini/hooks/inject-context.sh:
#!/usr/bin/env bash
# Get recent git commits for context
context=$(git log -5 --oneline 2>/dev/null || echo "No git history")
# Return as JSON
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "Recent commits:\n$context"
}
}
EOFRAG 기반 도구 필터링 (BeforeToolSelection)
BeforeToolSelection을 사용하여 지능적으로 도구 공간을 줄이세요. 이 예제는 Node.js 스크립트를 사용하여 사용자의 프롬프트를 확인하고 관련 도구만 허용합니다.
.gemini/hooks/filter-tools.js:
#!/usr/bin/env node
const fs = require('fs');
async function main() {
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
const { llm_request } = input;
// Decoupled API: Access messages from llm_request
const messages = llm_request.messages || [];
const lastUserMessage = messages
.slice()
.reverse()
.find((m) => m.role === 'user');
if (!lastUserMessage) {
console.log(JSON.stringify({})); // Do nothing
return;
}
const text = lastUserMessage.content;
const allowed = ['write_todos']; // Always allow memory
// Simple keyword matching
if (text.includes('read') || text.includes('check')) {
allowed.push('read_file', 'list_directory');
}
if (text.includes('test')) {
allowed.push('run_shell_command');
}
// If we found specific intent, filter tools. Otherwise allow all.
if (allowed.length > 1) {
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: 'BeforeToolSelection',
toolConfig: {
mode: 'ANY', // Force usage of one of these tools (or AUTO)
allowedFunctionNames: allowed,
},
},
}),
);
} else {
console.log(JSON.stringify({}));
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});.gemini/settings.json:
{
"hooks": {
"BeforeToolSelection": [
{
"matcher": "*",
"hooks": [
{
"name": "intent-filter",
"type": "command",
"command": "node .gemini/hooks/filter-tools.js"
}
]
}
]
}
}팁 (TIP)
통합 집계 전략 (Union Aggregation Strategy):
BeforeToolSelection은 일치하는 모든 훅의 결과를 결합한다는 점에서 독특합니다. 여러 필터링 훅이 있는 경우 에이전트는 허용된 모든 도구의 **합집합(union)**을 받게 됩니다.mode: "NONE"을 사용하는 경우에만 다른 훅을 무시하고 모든 도구를 비활성화합니다.
전체 예제: 스마트 개발 워크플로우 어시스턴트 (Complete example: Smart Development Workflow Assistant)
이 포괄적인 예제는 모든 훅 이벤트가 함께 작동하는 방식을 보여줍니다. 메모리를 유지하고, 도구를 필터링하고, 보안을 확인하는 시스템을 구축할 것입니다.
아키텍처 (Architecture)
- SessionStart: 프로젝트 메모리를 로드합니다.
- BeforeAgent: 메모리를 컨텍스트에 주입합니다.
- BeforeToolSelection: 의도에 따라 도구를 필터링합니다.
- BeforeTool: 비밀 정보를 검사합니다.
- AfterModel: 상호 작용을 기록합니다.
- AfterAgent: 최종 응답 품질을 검증합니다 (재시도).
- SessionEnd: 메모리를 통합합니다.
구성 (.gemini/settings.json)
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"name": "init",
"type": "command",
"command": "node .gemini/hooks/init.js"
}
]
}
],
"BeforeAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "memory",
"type": "command",
"command": "node .gemini/hooks/inject-memories.js"
}
]
}
],
"BeforeToolSelection": [
{
"matcher": "*",
"hooks": [
{
"name": "filter",
"type": "command",
"command": "node .gemini/hooks/rag-filter.js"
}
]
}
],
"BeforeTool": [
{
"matcher": "write_file",
"hooks": [
{
"name": "security",
"type": "command",
"command": "node .gemini/hooks/security.js"
}
]
}
],
"AfterModel": [
{
"matcher": "*",
"hooks": [
{
"name": "record",
"type": "command",
"command": "node .gemini/hooks/record.js"
}
]
}
],
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "validate",
"type": "command",
"command": "node .gemini/hooks/validate.js"
}
]
}
],
"SessionEnd": [
{
"matcher": "exit",
"hooks": [
{
"name": "save",
"type": "command",
"command": "node .gemini/hooks/consolidate.js"
}
]
}
]
}
}훅 스크립트 (Hook Scripts)
참고: 간결함을 위해 이 스크립트들은 로깅에
console.error를 사용하고 JSON 출력에 표준console.log를 사용합니다.
1. 초기화 (init.js)
#!/usr/bin/env node
// Initialize DB or resources
console.error('Initializing assistant...');
// Output to user
console.log(
JSON.stringify({
systemMessage: '🧠 Smart Assistant Loaded',
}),
);2. 메모리 주입 (inject-memories.js)
#!/usr/bin/env node
const fs = require('fs');
async function main() {
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
// Assume we fetch memories from a DB here
const memories = '- [Memory] Always use TypeScript for this project.';
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: 'BeforeAgent',
additionalContext: `\n## Relevant Memories\n${memories}`,
},
}),
);
}
main();3. 보안 검사 (security.js)
#!/usr/bin/env node
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0));
const content = input.tool_input.content || '';
if (content.includes('SECRET_KEY')) {
console.log(
JSON.stringify({
decision: 'deny',
reason: 'Found SECRET_KEY in content',
systemMessage: '🚨 Blocked sensitive commit',
}),
);
process.exit(0);
}
console.log(JSON.stringify({ decision: 'allow' }));4. 상호 작용 기록 (record.js)
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const input = JSON.parse(fs.readFileSync(0));
const { llm_request, llm_response } = input;
const logFile = path.join(
process.env.GEMINI_PROJECT_DIR,
'.gemini/memory/session.jsonl',
);
fs.appendFileSync(
logFile,
JSON.stringify({
request: llm_request,
response: llm_response,
timestamp: new Date().toISOString(),
}) + '\n',
);
console.log(JSON.stringify({}));5. 응답 검증 (validate.js)
#!/usr/bin/env node
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0));
const response = input.prompt_response;
// Example: Check if the agent forgot to include a summary
if (!response.includes('Summary:')) {
console.log(
JSON.stringify({
decision: 'block', // Triggers an automatic retry turn
reason: 'Your response is missing a Summary section. Please add one.',
systemMessage: '🔄 Requesting missing summary...',
}),
);
process.exit(0);
}
console.log(JSON.stringify({ decision: 'allow' }));6. 메모리 통합 (consolidate.js)
#!/usr/bin/env node
// Logic to save final session state
console.error('Consolidating memories for session end...');확장 프로그램으로 패키징하기 (Packaging as an extension)
프로젝트 수준의 훅은 특정 리포지토리에 적합하지만, 훅을 Gemini CLI 확장 프로그램 (opens in a new tab)으로 패키징하여 여러 프로젝트에서 공유할 수 있습니다. 이를 통해 버전 관리, 쉬운 배포 및 중앙 집중식 관리가 가능합니다.