Conversation
Claude/Codex AI 어시스턴트 설정 및 Kotest 도입
GitHub Actions CI 및 로컬 개발용 Docker Compose 추가
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive suite of AI coding agent configurations, skills, hooks, and project documentation to support both Claude and Codex agents, alongside Gradle updates for Kotest, MockK, and ktlint. The review feedback highlights several critical security and quality improvements, including fixing a command injection bypass in the command-guard hooks, resolving a security bypass in the pre-tool dispatcher, correcting Kotest testing guidelines to avoid class-level mock dependencies, and resolving documentation inconsistencies regarding controller response formats. Additionally, the reviewer recommended adding missing gh permissions to settings.json, utilizing the $REPO variable instead of placeholders in GitHub API calls, and replacing unsafe echo usage with printf when handling JSON inputs in bash hooks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| BLOCKED_PATTERNS=( | ||
| "rm -rf[[:space:]]*/[[:space:]]*$" | ||
| "sudo rm" | ||
| "> /dev/" | ||
| "dd if=" | ||
| "mkfs" | ||
| "curl.*\| sh" | ||
| "wget.*\| sh" | ||
| ) |
There was a problem hiding this comment.
rm -rf 패턴에 $ 앵커가 사용되어 있어, 뒤에 공백이나 추가 플래그(예: rm -rf / --no-preserve-root 또는 rm -rf / )를 붙이는 방식으로 쉽게 우회할 수 있는 보안 취약점이 존재합니다. 앵커를 제거하고 공백 또는 줄바꿈을 안전하게 처리할 수 있도록 패턴을 개선해야 합니다.
| BLOCKED_PATTERNS=( | |
| "rm -rf[[:space:]]*/[[:space:]]*$" | |
| "sudo rm" | |
| "> /dev/" | |
| "dd if=" | |
| "mkfs" | |
| "curl.*\| sh" | |
| "wget.*\| sh" | |
| ) | |
| BLOCKED_PATTERNS=( | |
| "rm -rf[[:space:]]+/[[:space:]]*([[:space:]]|$)" | |
| "sudo rm" | |
| "> /dev/" | |
| "dd if=" | |
| "mkfs" | |
| "curl.*\| sh" | |
| "wget.*\| sh" | |
| ) |
| BLOCKED_PATTERNS=( | ||
| "rm -rf[[:space:]]*/[[:space:]]*$" | ||
| "sudo rm" | ||
| "> /dev/" | ||
| "dd if=" | ||
| "mkfs" | ||
| "curl.*\| sh" | ||
| "wget.*\| sh" | ||
| ) |
There was a problem hiding this comment.
rm -rf 패턴에 $ 앵커가 사용되어 있어, 뒤에 공백이나 추가 플래그(예: rm -rf / --no-preserve-root 또는 rm -rf / )를 붙이는 방식으로 쉽게 우회할 수 있는 보안 취약점이 존재합니다. 앵커를 제거하고 공백 또는 줄바꿈을 안전하게 처리할 수 있도록 패턴을 개선해야 합니다.
| BLOCKED_PATTERNS=( | |
| "rm -rf[[:space:]]*/[[:space:]]*$" | |
| "sudo rm" | |
| "> /dev/" | |
| "dd if=" | |
| "mkfs" | |
| "curl.*\| sh" | |
| "wget.*\| sh" | |
| ) | |
| BLOCKED_PATTERNS=( | |
| "rm -rf[[:space:]]+/[[:space:]]*([[:space:]]|$)" | |
| "sudo rm" | |
| "> /dev/" | |
| "dd if=" | |
| "mkfs" | |
| "curl.*\| sh" | |
| "wget.*\| sh" | |
| ) |
| for hook in "$MODULES_DIR"/*/preToolUse.sh; do | ||
| [[ -f "$hook" ]] || continue | ||
| echo "$INPUT" | bash "$hook" | ||
| [[ $? -eq 2 ]] && exit 2 | ||
| done |
There was a problem hiding this comment.
현재 디스패처는 pre-tool 훅의 종료 코드가 2인 경우만 처리하고 다른 비정상 종료 코드(예: 1)는 무시합니다. 이로 인해 보안 검사 스크립트(예: secret-guard)에서 문법 오류나 실행 실패가 발생하더라도 도구 실행이 그대로 진행되어 보안 검사가 우회될 수 있습니다. 또한, $INPUT 변수를 전달할 때 echo 대신 printf를 사용하는 것이 안전합니다.
| for hook in "$MODULES_DIR"/*/preToolUse.sh; do | |
| [[ -f "$hook" ]] || continue | |
| echo "$INPUT" | bash "$hook" | |
| [[ $? -eq 2 ]] && exit 2 | |
| done | |
| for hook in "$MODULES_DIR"/*/preToolUse.sh; do | |
| [[ -f "$hook" ]] || continue | |
| printf '%s\n' "$INPUT" | bash "$hook" | |
| STATUS=$? | |
| [[ $STATUS -ne 0 ]] && exit $STATUS | |
| done |
| ### Mock Creation | ||
| ```kotlin | ||
| private val repository: ApiKeyRepository = mockk() | ||
| private val service = ApiKeyService(repository) | ||
| ``` |
There was a problem hiding this comment.
Kotest 가이드라인 및 프로젝트 컨벤션에 따르면, 테스트 의존성은 DescribeSpec 생성자 람다 내부의 로컬 변수로 선언하고 beforeEach에서 초기화해야 합니다. 클래스 멤버 프로퍼티(private val)로 선언하여 직접 초기화하는 방식은 테스트 간 상태 공유 문제를 유발할 수 있으며 프로젝트 규칙에 위배됩니다.
| ### Mock Creation | |
| ```kotlin | |
| private val repository: ApiKeyRepository = mockk() | |
| private val service = ApiKeyService(repository) | |
| ``` | |
| class ApiKeyServiceTest : DescribeSpec({ | |
| lateinit var repository: ApiKeyRepository | |
| lateinit var service: ApiKeyService | |
| beforeEach { | |
| repository = mockk() | |
| service = ApiKeyService(repository) | |
| } | |
| }) |
References
- Kotest 테스트에서 의존성은 DescribeSpec 생성자 람다 내부의 로컬 변수로 선언하고 beforeEach에서 초기화해야 합니다.
| ### Mock Creation | ||
| ```kotlin | ||
| private val repository: ApiKeyRepository = mockk() | ||
| private val service = ApiKeyService(repository) | ||
| ``` |
There was a problem hiding this comment.
Kotest 가이드라인 및 프로젝트 컨벤션에 따르면, 테스트 의존성은 DescribeSpec 생성자 람다 내부의 로컬 변수로 선언하고 beforeEach에서 초기화해야 합니다. 클래스 멤버 프로퍼티(private val)로 선언하여 직접 초기화하는 방식은 테스트 간 상태 공유 문제를 유발할 수 있으며 프로젝트 규칙에 위배됩니다.
| ### Mock Creation | |
| ```kotlin | |
| private val repository: ApiKeyRepository = mockk() | |
| private val service = ApiKeyService(repository) | |
| ``` | |
| class ApiKeyServiceTest : DescribeSpec({ | |
| lateinit var repository: ApiKeyRepository | |
| lateinit var service: ApiKeyService | |
| beforeEach { | |
| repository = mockk() | |
| service = ApiKeyService(repository) | |
| } | |
| }) |
References
- Kotest 테스트에서 의존성은 DescribeSpec 생성자 람다 내부의 로컬 변수로 선언하고 beforeEach에서 초기화해야 합니다.
| "permissions": { | ||
| "allow": [ | ||
| "WebSearch", | ||
| "WebFetch", | ||
| "Bash(./gradlew *)", | ||
| "Bash(git diff*)", | ||
| "Bash(git status)", | ||
| "Bash(git log*)", | ||
| "Bash(git add*)", | ||
| "Bash(git commit*)", | ||
| "Bash(git branch*)", | ||
| "Bash(git checkout*)", | ||
| "Bash(git rev-parse*)" | ||
| ] | ||
| }, |
There was a problem hiding this comment.
프로젝트 내의 resolve-reviews 및 write-pr 스킬은 GitHub CLI(gh) 명령어를 빈번하게 사용합니다. 하지만 허용된 권한 목록에 gh 관련 권한이 누락되어 있어, 실행 시마다 사용자에게 권한 확인을 요청하거나 비대화형 환경에서 실패할 수 있습니다. "Bash(gh *)" 권한을 추가하는 것이 좋습니다.
"permissions": {
"allow": [
"WebSearch",
"WebFetch",
"Bash(./gradlew *)",
"Bash(git diff*)",
"Bash(git status)",
"Bash(git log*)",
"Bash(git add*)",
"Bash(git commit*)",
"Bash(git branch*)",
"Bash(git checkout*)",
"Bash(git rev-parse*)",
"Bash(gh *)"
]
},| ```bash | ||
| gh api "repos/<owner>/<repo>/pulls/<pr_number>/comments/<comment_id>/replies" \ | ||
| -f body="<reply_body>" | ||
| ``` |
There was a problem hiding this comment.
GitHub API 호출 시 <owner> 및 <repo> 플레이스홀더를 사용하는 대신, Step 1에서 이미 수집한 $REPO 변수를 활용하면 수동 파싱 및 치환 과정 없이 더 안전하고 간결하게 명령어를 실행할 수 있습니다.
| ```bash | |
| gh api "repos/<owner>/<repo>/pulls/<pr_number>/comments/<comment_id>/replies" \ | |
| -f body="<reply_body>" | |
| ``` | |
| gh api "repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \ | |
| -f body="<reply_body>" |
| ```bash | ||
| gh api "repos/<owner>/<repo>/pulls/<pr_number>/comments/<comment_id>/replies" \ | ||
| -f body="<reply_body>" | ||
| ``` |
There was a problem hiding this comment.
GitHub API 호출 시 <owner> 및 <repo> 플레이스홀더를 사용하는 대신, Step 1에서 이미 수집한 $REPO 변수를 활용하면 수동 파싱 및 치환 과정 없이 더 안전하고 간결하게 명령어를 실행할 수 있습니다.
| ```bash | |
| gh api "repos/<owner>/<repo>/pulls/<pr_number>/comments/<comment_id>/replies" \ | |
| -f body="<reply_body>" | |
| ``` | |
| gh api "repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \ | |
| -f body="<reply_body>" |
| INPUT=$(cat) | ||
| TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') | ||
| if [[ "$TOOL_NAME" == "Write" ]] || [[ "$TOOL_NAME" == "Edit" ]]; then | ||
| CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') | ||
| FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') |
There was a problem hiding this comment.
Bash에서 JSON 문자열이 포함된 $INPUT 변수를 echo로 출력하면, 문자열이 하이픈(-)으로 시작하거나 백슬래시가 포함된 경우 오동작하거나 JSON이 깨질 수 있습니다. 안전하고 일관된 출력을 위해 printf '%s\n'을 사용하는 것이 좋습니다.
| INPUT=$(cat) | |
| TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') | |
| if [[ "$TOOL_NAME" == "Write" ]] || [[ "$TOOL_NAME" == "Edit" ]]; then | |
| CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') | |
| FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') | |
| INPUT=$(cat) | |
| TOOL_NAME=$(printf '%s\n' "$INPUT" | jq -r '.tool_name') | |
| if [[ "$TOOL_NAME" == "Write" ]] || [[ "$TOOL_NAME" == "Edit" ]]; then | |
| CONTENT=$(printf '%s\n' "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') | |
| FILE_PATH=$(printf '%s\n' "$INPUT" | jq -r '.tool_input.file_path // empty') |
| TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') | ||
|
|
||
| if [[ "$TOOL_NAME" == "Write" ]] || [[ "$TOOL_NAME" == "Edit" ]] || [[ "$TOOL_NAME" == "write_file" ]]; then | ||
| CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') | ||
| FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty') | ||
|
|
There was a problem hiding this comment.
Bash에서 JSON 문자열이 포함된 $INPUT 변수를 echo로 출력하면, 문자열이 하이픈(-)으로 시작하거나 백슬래시가 포함된 경우 오동작하거나 JSON이 깨질 수 있습니다. 안전하고 일관된 출력을 위해 printf '%s\n'을 사용하는 것이 좋습니다.
| TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') | |
| if [[ "$TOOL_NAME" == "Write" ]] || [[ "$TOOL_NAME" == "Edit" ]] || [[ "$TOOL_NAME" == "write_file" ]]; then | |
| CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') | |
| FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty') | |
| TOOL_NAME=$(printf '%s\n' "$INPUT" | jq -r '.tool_name') | |
| if [[ "$TOOL_NAME" == "Write" ]] || [[ "$TOOL_NAME" == "Edit" ]] || [[ "$TOOL_NAME" == "write_file" ]]; then | |
| CONTENT=$(printf '%s\n' "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') | |
| FILE_PATH=$(printf '%s\n' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty') |
✨ 작업 내용
🔍 리뷰 시 참고사항
✅ 체크리스트
.env.example등) 변경이 필요한 경우 작성 또는 수정했나요?📎 관련 이슈(선택)