Skip to content

feat: Hermes Agent 기반 comment-fix 파이프라인 통합#47

Merged
bae080311 merged 2 commits into
mainfrom
feat/hermes-harness-integration
Jul 8, 2026
Merged

feat: Hermes Agent 기반 comment-fix 파이프라인 통합#47
bae080311 merged 2 commits into
mainfrom
feat/hermes-harness-integration

Conversation

@bae080311

Copy link
Copy Markdown
Owner

Summary

  • /fix-build, /fix-lint, /fix PR 코멘트 커맨드의 실제 진단/수정/커밋을 n8n+Gemini에서 Hermes Agent로 전환한다. n8n은 웹훅 트리거 릴레이와 최종 결과 반응(🚀/-1)만 담당하고, 실제 셸/git-push 실행은 별도 Fly 앱(jump-section-hermes)으로 분리해 블라스트 레이디어스를 낮춘다.
  • Claude Code 쪽 설정을 CLAUDE.md에 명문화한 Rule/Skill/Agent/Hook 4계층 구조로 재구성했다(.claude/commands/ 슬래시 커맨드 → .claude/skills/implement-issue, .claude/agents/issue-implementer, .claude/agents/pr-comment-fixer).
  • 보호 브랜치(main/master/develop/release/**) 직접 push를 두 실행 주체 모두에서 하드 차단한다 — Claude Code는 PreToolUse 훅(.claude/scripts/block-protected-push.sh), Hermes는 scripts/hermes-safe-push.sh.
  • .github/workflows/slash-fix.yml의 n8n 웹훅 호출에 HMAC 서명(X-Hub-Signature-256)을 추가하고, Hermes 완료 콜백을 받아 반응을 다는 .n8n/workflows/hermes-fix-callback.json을 신규 추가했다.

Test plan

  • pnpm format — 통과 (일부 마크다운 재포맷)
  • pnpm lint — 통과
  • pnpm build — 통과
  • pnpm check-exports — 통과
  • pnpm test — 61개 테스트 전체 통과
  • .hermes/config.template.yaml 실제 값 치환 후 Fly 앱(jump-section-hermes) 배포 및 웹훅 왕복 확인 (배포는 이 PR 범위 밖)
  • GitHub 저장소 Settings에 GITHUB_WEBHOOK_SECRET Actions Secret 등록 (코드 변경만으로는 검증이 동작하지 않음)

🤖 Generated with Claude Code

n8n + Gemini로 처리하던 /fix-build, /fix-lint, /fix PR 코멘트 커맨드를 Hermes Agent가
GitHub MCP로 직접 진단/수정/커밋하도록 전환한다. n8n은 트리거 릴레이와 결과 반응만 담당하고,
실제 셸/git-push 실행은 별도 Fly 앱(jump-section-hermes)으로 블라스트 레이디어스를 분리했다.

- Claude Code 쪽 하네스를 Rule/Skill/Agent/Hook 4계층으로 재구성(.claude/skills, .claude/agents)
- 보호 브랜치 push를 PreToolUse 훅(block-protected-push.sh)과 Hermes 쪽 hermes-safe-push.sh로
  이중 강제
- GitHub Actions -> n8n 웹훅에 HMAC 서명 검증 추가, Hermes 완료 콜백 워크플로우 신규 추가
- CLAUDE.md에 두 실행 주체(Claude Code/Hermes)가 공유하는 거버넌스 규칙 명문화
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
jump-section-docs Ready Ready Preview, Comment Jul 8, 2026 10:52am

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates the Hermes Agent into the repository's AI pipeline to handle PR comment commands, refactoring the n8n workflows to delegate code diagnostics and fixing to Hermes, and introducing governance rules and scripts to prevent direct pushes to protected branches. The review feedback highlights several critical security and stability improvements, including using n8n's binary buffer helper to safely retrieve webhook payloads, enhancing the push-blocking script to handle the -f flag and trailing push options, correcting mismatched GitHub MCP tool names in the Hermes configuration, and ensuring case-insensitive remote URL validation in the safe push script.

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.

},
{
"parameters": {
"jsCode": "// GitHub Actions(slash-fix.yml)가 계산한 X-Hub-Signature-256을 검증한다.\n// rawBody 옵션으로 받은 원본 바이트로 HMAC을 계산해야 GitHub이 보낸 서명과 정확히 일치한다\n// (파싱 후 재직렬화된 JSON은 바이트가 달라져 서명이 깨질 수 있음).\n// $binary 필드 위치는 n8n 버전에 따라 다를 수 있으니 설치된 버전으로 재확인할 것.\nconst crypto = require('crypto');\nconst secret = $env.GITHUB_WEBHOOK_SECRET;\n\nconst item = $input.first();\nconst rawBuffer = Buffer.from(item.binary.data.data, 'base64');\nconst rawBody = rawBuffer.toString('utf8');\nconst signatureHeader = (item.json.headers && item.json.headers['x-hub-signature-256']) || '';\n\nconst expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');\n\nconst sigBuf = Buffer.from(signatureHeader);\nconst expBuf = Buffer.from(expected);\nconst valid = sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf);\n\nif (!valid) {\n // 서명 불일치 - 흐름을 중단한다 (빈 배열을 반환하면 이후 노드가 실행되지 않음)\n return [];\n}\n\nreturn [{ json: JSON.parse(rawBody) }];"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

n8n의 바이너리 데이터 저장 모드가 filesystem 또는 filesystem-v2로 설정되어 있을 경우, item.binary.data.data를 직접 참조하면 실제 파일 내용이 아닌 "filesystem-v2, 9 bytes"와 같은 참조 문자열이 반환됩니다. 이로 인해 Buffer.from으로 디코딩한 결과가 깨져 HMAC 서명 검증이 항상 실패하게 됩니다.

생산 환경에서의 안정성을 위해 await this.helpers.getBinaryDataBuffer(0, 'data') 헬퍼 함수를 사용하여 실제 바이너리 버퍼를 안전하게 가져오도록 수정해야 합니다.

Suggested change
"jsCode": "// GitHub Actions(slash-fix.yml)가 계산한 X-Hub-Signature-256을 검증한다.\n// rawBody 옵션으로 받은 원본 바이트로 HMAC을 계산해야 GitHub이 보낸 서명과 정확히 일치한다\n// (파싱 후 재직렬화된 JSON은 바이트가 달라져 서명이 깨질 수 있음).\n// $binary 필드 위치는 n8n 버전에 따라 다를 수 있으니 설치된 버전으로 재확인할 것.\nconst crypto = require('crypto');\nconst secret = $env.GITHUB_WEBHOOK_SECRET;\n\nconst item = $input.first();\nconst rawBuffer = Buffer.from(item.binary.data.data, 'base64');\nconst rawBody = rawBuffer.toString('utf8');\nconst signatureHeader = (item.json.headers && item.json.headers['x-hub-signature-256']) || '';\n\nconst expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');\n\nconst sigBuf = Buffer.from(signatureHeader);\nconst expBuf = Buffer.from(expected);\nconst valid = sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf);\n\nif (!valid) {\n // 서명 불일치 - 흐름을 중단한다 (빈 배열을 반환하면 이후 노드가 실행되지 않음)\n return [];\n}\n\nreturn [{ json: JSON.parse(rawBody) }];"
"jsCode": "// GitHub Actions(slash-fix.yml)가 계산한 X-Hub-Signature-256을 검증한다.\n// rawBody 옵션으로 받은 원본 바이트로 HMAC을 계산해야 GitHub이 보낸 서명과 정확히 일치한다\n// (파싱 후 재직렬화된 JSON은 바이트가 달라져 서명이 깨질 수 있음).\n// $binary 필드 위치는 n8n 버전에 따라 다를 수 있으니 설치된 버전으로 재확인할 것.\nconst crypto = require('crypto');\nconst secret = $env.GITHUB_WEBHOOK_SECRET;\n\nconst item = $input.first();\nconst rawBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst rawBody = rawBuffer.toString('utf8');\nconst signatureHeader = (item.json.headers && item.json.headers['x-hub-signature-256']) || '';\n\nconst expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');\n\nconst sigBuf = Buffer.from(signatureHeader);\nconst expBuf = Buffer.from(expected);\nconst valid = sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf);\n\nif (!valid) {\n // 서명 불일치 - 흐름을 중단한다 (빈 배열을 반환하면 이후 노드가 실행되지 않음)\n return [];\n}\n\nreturn [{ json: JSON.parse(rawBody) }];"

Comment thread .claude/scripts/block-protected-push.sh Outdated
Comment on lines +19 to +23
# --force / --force-with-lease는 무조건 차단
if [[ "$CMD" == *"--force"* ]]; then
echo "[block-protected-push] --force/--force-with-lease 푸시는 허용되지 않습니다." >&2
exit 2
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

강제 푸시를 차단할 때 --force 외에 단축 옵션인 -f 패턴에 대한 검증이 누락되어 있습니다. Claude Code가 git push origin branch -f와 같이 실행할 경우 차단 로직을 우회할 수 있으므로, -f 패턴도 함께 차단하도록 정규식을 추가하는 것이 안전합니다.

Suggested change
# --force / --force-with-lease는 무조건 차단
if [[ "$CMD" == *"--force"* ]]; then
echo "[block-protected-push] --force/--force-with-lease 푸시는 허용되지 않습니다." >&2
exit 2
fi
# --force / --force-with-lease / -f는 무조건 차단
if [[ "$CMD" == *"--force"* || "$CMD" =~ \ -f(\ |$) ]]; then
echo "[block-protected-push] --force/--force-with-lease/-f 푸시는 허용되지 않습니다." >&2
exit 2
fi

Comment thread .claude/scripts/block-protected-push.sh Outdated
Comment on lines +27 to +33
# "git push <remote> <branch>" 형태에서 마지막 인자를 브랜치로 간주
TARGET_BRANCH=$(echo "$CMD" | grep -oE 'git push[^&|;]*' | head -1 | awk '{print $NF}')

# 브랜치가 명시되지 않은 "git push"/"git push origin" 형태면 현재 체크아웃된 브랜치를 확인
if [[ -z "$TARGET_BRANCH" || "$TARGET_BRANCH" == "push" || "$TARGET_BRANCH" == "origin" ]]; then
TARGET_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

git push origin main --tags 또는 git push origin main -u와 같이 브랜치명 뒤에 옵션이 추가되는 경우, awk '{print $NF}'가 옵션(--tags, -u)을 브랜치명으로 오인하여 보호 브랜치 검증을 우회할 수 있습니다. 또한 local:main과 같은 refspec 형태의 푸시도 우회될 수 있습니다.

이를 방지하기 위해 옵션을 제외한 상태에서 브랜치명을 추출하고, refspec 대응을 추가하는 것이 안전합니다.

Suggested change
# "git push <remote> <branch>" 형태에서 마지막 인자를 브랜치로 간주
TARGET_BRANCH=$(echo "$CMD" | grep -oE 'git push[^&|;]*' | head -1 | awk '{print $NF}')
# 브랜치가 명시되지 않은 "git push"/"git push origin" 형태면 현재 체크아웃된 브랜치를 확인
if [[ -z "$TARGET_BRANCH" || "$TARGET_BRANCH" == "push" || "$TARGET_BRANCH" == "origin" ]]; then
TARGET_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
fi
# "git push <remote> <branch>" 형태에서 옵션(-u, --tags 등)을 제외한 마지막 인자를 브랜치로 간주
CLEANED_CMD=$(echo "$CMD" | grep -oE 'git push[^&|;]*' | head -1 | tr ' ' '\n' | grep -v '^-' | tr '\n' ' ')
TARGET_BRANCH=$(echo "$CLEANED_CMD" | awk '{print $NF}')
# refspec (예: local:remote) 형태인 경우 remote 브랜치만 추출
TARGET_BRANCH=${TARGET_BRANCH#*:}
# 브랜치가 명시되지 않은 "git push"/"git push origin" 형태면 현재 체크아웃된 브랜치를 확인
if [[ -z "$TARGET_BRANCH" || "$TARGET_BRANCH" == "push" || "$TARGET_BRANCH" == "origin" ]]; then
TARGET_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
fi

Comment on lines +34 to +40
allowed_tools:
- mcp_github_get_pull_request
- mcp_github_get_pull_request_files
- mcp_github_list_check_runs
- mcp_github_create_issue_comment
- mcp_github_reply_to_review_comment
- mcp_github_add_reaction

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

allowed_tools에 정의된 일부 도구명이 공식 github-mcp-server 스펙과 일치하지 않아, 에이전트 실행 시 "도구 권한 없음" 또는 "도구를 찾을 수 없음" 에러가 발생할 수 있습니다.

  • mcp_github_create_issue_comment -> mcp_github_add_issue_comment (또는 add_issue_comment)
  • mcp_github_reply_to_review_comment -> mcp_github_add_reply_to_pull_request_comment (또는 add_reply_to_pull_request_comment)
  • mcp_github_list_check_runs -> mcp_github_get_check_runs (또는 get_check_runs)

사용 중인 GitHub MCP 서버의 실제 도구 정의를 확인하고 일치하도록 수정해야 합니다.

    allowed_tools:
      - mcp_github_get_pull_request
      - mcp_github_get_pull_request_files
      - mcp_github_get_check_runs
      - mcp_github_add_issue_comment
      - mcp_github_add_reply_to_pull_request_comment
      - mcp_github_add_reaction

Comment on lines +25 to +30
# 다른 저장소로 오발송되는 것을 방지 (payload 검증 원칙: 내용은 신뢰하지 않는다)
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$REMOTE_URL" != *"${OWNER}/${REPO}"* ]]; then
echo "[hermes-safe-push] origin(${REMOTE_URL})이 예상 저장소(${OWNER}/${REPO})와 일치하지 않습니다." >&2
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

GitHub 저장소명과 소유자명은 대소문자를 구분하지 않지만, Bash의 [[ ... ]] 비교는 기본적으로 대소문자를 구분합니다. 만약 로컬이나 CI 환경의 원격지 URL(origin)에 대소문자가 섞여 있을 경우(예: Bae080311/Jump-Section), 이 검증이 실패하여 정상적인 푸시가 차단될 수 있습니다.

안정성을 위해 비교 전에 양쪽 문자열을 모두 소문자로 변환하여 비교하는 것이 좋습니다.

Suggested change
# 다른 저장소로 오발송되는 것을 방지 (payload 검증 원칙: 내용은 신뢰하지 않는다)
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$REMOTE_URL" != *"${OWNER}/${REPO}"* ]]; then
echo "[hermes-safe-push] origin(${REMOTE_URL})이 예상 저장소(${OWNER}/${REPO})와 일치하지 않습니다." >&2
exit 1
fi
# 다른 저장소로 오발송되는 것을 방지 (payload 검증 원칙: 내용은 신뢰하지 않는다)
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
REMOTE_URL_LOWER=$(echo "$REMOTE_URL" | tr '[:upper:]' '[:lower:]')
EXPECTED_LOWER=$(echo "${OWNER}/${REPO}" | tr '[:upper:]' '[:lower:]')
if [[ "$REMOTE_URL_LOWER" != *"$EXPECTED_LOWER"* ]]; then
echo "[hermes-safe-push] origin(${REMOTE_URL})이 예상 저장소(${OWNER}/${REPO})와 일치하지 않습니다." >&2
exit 1
fi

- n8n 서명 검증 노드가 item.binary.data.data를 직접 참조하던 것을
  getBinaryDataBuffer 헬퍼로 교체 (filesystem 모드에서 서명 검증이
  항상 실패하던 버그)
- block-protected-push.sh: --force 외 -f 단축 플래그 차단 추가,
  브랜치 파싱 시 트레일링 옵션(--tags 등)/refspec(local:main)이
  브랜치명으로 오인되던 문제 수정
- Hermes GitHub MCP allowed_tools 도구명을 실제 스펙에 맞게 교정
  (config.template.yaml, fix-build 스킬 문서 동기화)
- hermes-safe-push.sh: origin 저장소 비교를 대소문자 무시로 변경

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bae080311
bae080311 marked this pull request as ready for review July 8, 2026 10:59
@bae080311
bae080311 merged commit 620a557 into main Jul 8, 2026
3 checks passed
@bae080311
bae080311 deleted the feat/hermes-harness-integration branch July 8, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant