Docs
자습서
훅(Hooks) 작성 방법

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"
  }
}
EOF

RAG 기반 도구 필터링 (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)

  1. SessionStart: 프로젝트 메모리를 로드합니다.
  2. BeforeAgent: 메모리를 컨텍스트에 주입합니다.
  3. BeforeToolSelection: 의도에 따라 도구를 필터링합니다.
  4. BeforeTool: 비밀 정보를 검사합니다.
  5. AfterModel: 상호 작용을 기록합니다.
  6. AfterAgent: 최종 응답 품질을 검증합니다 (재시도).
  7. 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)으로 패키징하여 여러 프로젝트에서 공유할 수 있습니다. 이를 통해 버전 관리, 쉬운 배포 및 중앙 집중식 관리가 가능합니다.