
ghq와 fzf로 Claude를 실행하는 herdr 플러그인을 만들며 배운 점
요약
ghq와 fzf를 활용하여 특정 리포지토리에서 Claude Code를 즉시 실행할 수 있는 herdr 플러그인 제작 과정을 소개합니다. 터미널 멀티플렉서 herdr의 워크스페이스와 탭 기능을 활용해 코딩 에이전트 작업 흐름을 자동화하는 방법을 다룹니다.
핵심 포인트
- ghq와 fzf를 이용한 리포지토리 선택 자동화
- herdr 플러그인 구조 및 매니페스트 활용법
- Claude Code를 특정 워크스페이스에서 즉시 실행하는 워크플로우
- TUI 구현을 위한 팝업 패널 활용 기술
herdr는 코딩 에이전트(Coding Agent)를 위한 터미널 멀티플렉서(Terminal Multiplexer)입니다. 제가 herdr로 작업을 시작할 때의 흐름은 대략 정해져 있으며, 그 흐름을 원키(One-key)로 실행하는 herdr 플러그인인 herdr-ghq-open-agent를 만들었습니다. ghq와 fzf로 리포지토리(Repository)를 선택하고, herdr의 워크스페이스(Workspace) 또는 탭(Tab)에서 Claude Code를 실행하는 흐름입니다. 여러분도 비슷한 처리를 스스로 혹은 AI 에이전트에 대한 지시를 통해 반복하고 계시지 않나요?
이 기사에서는 해당 플러그인의 소개와 만드는 과정에서 얻은 배움을 정리합니다.
만든 것
herdr-ghq-open-agent는 설정한 키 바인딩(Key binding)으로 팝업(Popup)을 열고, fzf로 리포지토리를 선택하면 다음과 같이 동작합니다.
- 선택한 리포지토리 디렉토리에서 동작 중인 워크스페이스가 있다면, 그곳에 새로운 탭을 추가한다
- 없다면 새로운 워크스페이스를 만든다
- 패널(Pane)의 셸(Shell)이 실행되면,
herdr agent start
로 Claude Code를 실행한다
설치는 한 개의 명령어로 가능합니다.
herdr plugin install kenchan/herdr-ghq-open-agent
호출은 키에 할당하므로, ~/.config/herdr/config.toml에 다음과 같이 작성합니다.
[[keys.command]]
key = "prefix+t"
type = "shell"
...
순수한 herdr가 아니라 $HERDR_BIN_PATH를 사용하는 이유는 후술하겠습니다. 구현은 picker.sh (bash, 112행)와 herdr-plugin.toml 두 파일로 구성되며, 별도의 설정 파일은 없습니다. 현재는 fzf와 Claude Code 사용을 전제로 하고 있습니다.
구조
herdr의 플러그인은 herdr-plugin.toml이라는 매니페스트(Manifest)와 임의의 언어로 작성된 명령의 조합입니다. 매니페스트가 패널(Pane)이나 액션(Action) 등의 엔트리 포인트(Entry point)를 선언하며, 실행된 명령은 herdr workspace create와 같은 herdr CLI를 호출하여 세션을 조작합니다. 이번 매니페스트는 다음과 같습니다.
id = "kenchan.ghq-open-agent"
name = "ghq Open Agent"
version = "0.1.0"
...
fzf와 같은 TUI(Text User Interface)에는 실제 터미널이 필요하므로, plugin action이 아닌 placement = "popup"의 패널(80%×80% 크기의 플로팅 터미널)로 정의했습니다.
picker.sh는 셸 스크립트(Shell script)로 작성된 일련의 명령 호출입니다.
ghq list --full-path | fzf로 리포지토리를 선택한다pane list로 모든 패널의 cwd(Current Working Directory)를 대조하여, 선택한 리포지토리에서 동작 중인 워크스페이스를 찾는다- 찾으면
tab create로 해당 워크스페이스에 탭을 추가하고, 없으면workspace create로 신규 생성한다 pane wait-output --regex '\S'로 패널에 무언가 출력될 때까지(=셸 프롬프트가 나올 때까지) 기다린다agent start --kind claude --pane <pane_id>로 Claude Code를 실행한다
herdr agent start는 셸 프롬프트 상태인 패널 안에서 에이전트를 실행하는 명령입니다. 패널 안에서 Claude를 실행하고, 동일한 터미널 내에서 Claude가 검출되어 입력 대기 상태가 될 때까지 블록(Block)합니다. 패널에 직접 claude라고 입력하는 경우에도 herdr는 실행 중인 에이전트를 자동으로 검출합니다. herdr agent start를 사용하는 이점은 실행의 성공 여부가 명령의 종료로 확정된다는 점과, 나중에 조작하기 위한 이름을 실행 시점에 지정할 수 있다는 점입니다.
cwd로 워크스페이스를 검색하는 전용 API는 없지만, pane list는 모든 패널을 cwd 및 workspace_id와 함께 반환합니다. 선택한 경로와 cwd를 대조하여 처음으로 일치하는 패널의 워크스페이스를 재사용하도록 구현했습니다.
existing_workspace_id=$("$HERDR" pane list | jq -r --arg p "$selected_path"
'[.result.panes[] | select(.cwd == $p or (.cwd | startswith($p + "/")))][0].workspace_id // empty')
agent 이름에는 [a-z][a-z0-9_-]{0,31}라는 제약이 있기 때문에, 리포지토리 이름을 소문자로 변환하고 영문자와 숫자 이외의 문자는 -로 치환하여 ghq-<리포지토리 이름> 형태로 정규화합니다. 기존의 agent 이름과 충돌하는 경우에는 -2, -3과 같이 접미사(suffix)를 붙입니다. 또한 pane wait-output이 매칭된 직후라도 pane의 쉘이 아직 agent의 기동을 받아들일 수 없는 경우가 있어, agent start가 agent_pane_busy를 반환합니다. 이 에러 코드에 한해서는 최대 10회 재시도(retry)합니다.
설정 메커니즘은 따로 두지 않았습니다. herdr v1의 config.toml에는 플러그인만의 독자적인 설정을 갖기 위한 섹션이 없으며, HERDR_PLUGIN_CONFIG_DIR 하위에 자체 파일을 두는 방식을 사용합니다. 검색 명령어를 fzf 이외의 것으로 전환하거나, Claude Code 이외의 에이전트(agent)를 이용하는 기능은 언젠가 필요해질 것입니다. 다만, 플러그인이 설정을 갖기 위한 표준적인 방법이 조만간 확립될 것이라 생각하여 보류해 두었습니다.
배움 1: type = "shell"은 로그인 쉘(login shell)에서 실행된다
설치 직후, 이 함정에 빠졌습니다. type = "shell" 키 바인딩으로부터 herdr plugin pane open ...을 실행해도 아무 일도 일어나지 않습니다. 표준 출력(stdout)이나 에러도 나오지 않고, 팝업(popup)도 열리지 않습니다.
herdr의 소스 코드를 읽어보니, shell 타입의 커스텀 명령은 /bin/sh -lc '<command>'로 실행되고 있었습니다. -l은 로그인 쉘로서의 기동을 의미하며, /etc/profile이 읽힙니다. 많은 환경의 /etc/profile은 PATH를 시스템 기본값으로 덮어쓰기 때문에, 호출 측으로부터 상속받은 PATH는 거기서 사라집니다.
$ /bin/sh -c 'echo $PATH' # 호출 측의 PATH를 그대로 상속함
/home/kenchan/.local/bin:/home/kenchan/.local/share/mise/installs/...(중략)
$ /bin/sh -lc 'echo $PATH' # /etc/profile이 PATH를 덮어씀
...
저는 mise로 herdr를 도입했기 때문에, 실체는 mise가 관리하는 디렉토리(~/.local/share/mise/installs/ 하위)에 있습니다. 로그인 쉘의 PATH에는 이 디렉토리가 포함되지 않아, herdr 명령어를 찾을 수 없는(resolve할 수 없는) 문제가 발생했습니다.
"만든 것"의 키 바인딩 예시에서 순수한 herdr가 아니라 "$HERDR_BIN_PATH"를 사용하고 있는 이유는 이 함정을 피하기 위해서입니다. HERDR_BIN_PATH는 herdr가 키 바인딩이나 플러그인의 명령 환경에 주입하는, 실행 중인 바이너리의 절대 경로입니다. PATH 탐색(resolution)을 거치지 않기 때문에 /etc/profile이 무엇을 하든 영향을 받지 않습니다. 같은 이유로 picker.sh 내의 herdr 호출도 모두 ${HERDR_BIN_PATH:-herdr}를 경유하도록 했습니다. mise, asdf와 같은 버전 관리 도구(version manager)로 herdr를 도입하는 경우에는 반드시 겪게 되는 함정이므로, herdr 플러그인을 제작하는 분들은 기억해 두는 것이 좋습니다.
배움 2: 팝업(popup)의 수명과 동기 처리(synchronous processing)는 분리한다
herdr의 팝업 배치(popup placement)는 명령이 종료됨과 동시에 닫힙니다. 뒤집어 말하면, 명령이 동작하는 동안에는 닫히지 않습니다. 이번과 같은 처리를 그대로 직렬(serial)로 구현하면, 리포지토리 선택 후에도 동기 처리가 계속됩니다. 워크스페이스(workspace) 생성, 쉘 기동 대기, 그리고 agent start가 이어집니다. agent start는 agent의 탐지 완료까지 블로킹(blocking)하며, 빨라도 몇 초가 걸립니다. 그렇게 되면 fzf가 사라진 후의 빈 팝업이 화면에 계속 남아있게 되어 신경 쓰이게 됩니다.
그래서 스크립트를 2단계로 나누었습니다. popup 단계는 fzf 선택이 끝나면, 자기 자신을 --open 플래그를 붙여 분리하여 실행(detached 실행)하고 즉시 exit합니다. 이렇게 하면 popup은 선택 직후에 닫힙니다. --open으로 실행된 worker 단계가 workspace와 tab 생성, agent 실행을 백그라운드에서 계속 진행합니다.
if [ "${1:-}" = "--open" ]; then
open_repo "$2" # workspace/tab 생성 및 agent 실행
exit 0
...
worker에는 TTY가 없으므로, 에러는 herdr notification show 토스트(toast)로 알립니다. 다만 popup이 파괴되는 도중에 보낸 토스트는 API가 성공을 반환하더라도 표시되지 않을 수 있습니다. 응답의 .result.shown이 false인 경우에 한해, 0.1초를 기다린 후 1회 재전송합니다.
이 코드의 trap '' HUP이 다음 배움의 주제입니다.
배움 3: fork()와 setsid() 사이에서 SIGHUP이 끼어들다
detached 실행은 처음에 trap '' HUP 없이 구현했습니다. 그런데 실제로 작동시켜 보니, worker 단계가 호출되었을 텐데도 workspace 생성이나 agent 실행이 아무것도 일어나지 않았습니다. 로그에도 실행 흔적이 남지 않아, 마치 호출 자체가 없었던 것처럼 보였습니다.
로그를 심거나 다른 명령어로 테스트하며 검증해 나간 결과, 다음과 같은 일이 일어나고 있다는 것을 알게 되었습니다.
- popup 단계의 bash가
exit 0한다. - herdr가 popup의 pane을 파괴하고, PTY(가상 터미널)를 닫는다.
- 커널이 해당 PTY에 연결되어 있던 프로세스에 SIGHUP을 보낸다.
- 아직
setsid를 호출하기 전인 worker도 이 '연결되어 있던 프로세스'에 포함되어 있다. - worker는 SIGHUP에 대해 아무런 핸들러(handler)를 설정하지 않았으므로, 기본 동작인 종료를 수행한다.
&로 실행한 자식 프로세스는 setsid를 실행하고 나서야 비로소 원래의 터미널에서 분리됩니다. bash가 자식 프로세스를 fork()한 후, 그 자식이 실제로 setsid를 호출하기까지는 아주 짧은 시간 차가 존재합니다. 그 틈 사이에서 PTY가 파괴되면, 자식은 아직 원래 터미널에 연결된 상태로 SIGHUP을 받게 됩니다.
수정 방법은 spawn 직전에 배치한 trap '' HUP입니다. 'SIGHUP을 무시한다'는 설정은 fork()나 exec를 거쳐도 사라지지 않습니다 (exec로 리셋되는 것은 핸들러의 내용이며, 무시하겠다는 지정만은 특별히 계승됩니다). 이로써 setsid를 호출하기 전에 SIGHUP이 도착하더라도 worker는 죽지 않고 살아남을 수 있습니다. setsid를 호출한 후에는 제어 터미널(controlling terminal)이 없는 새로운 세션으로 이동하므로, 애초에 PTY의 파괴 자체가 전달되지 않게 됩니다.
배움 4: 구현 언어의 적정선과 공통된 함정
bash 112행이라는 구현이 herdr 플러그인으로서 적절한 규모인지, 애초에 쉘 스크립트(shell script)로 구현하는 것이 좋은지 궁금했습니다. 그래서 GitHub의 topic herdr-plugin이 붙은 리포지토리를 star 순으로 10개와, 유사한 종류의 플러그인인 crafts69guy/herdr-ghq를 조사했습니다.
star 순 결과는 다음과 같습니다 (star 수와 행수는 2026년 8월 조사 시점 기준).
| 리포지토리 | star | 구현 |
|---|---|---|
| openclaw/crabbox | 1250 | sh 124행 + Go CLI로 위임 |
| ... |
fzf와 같은 기존의 피커(picker)를 활용한다면 쉘 스크립트로 충분합니다. Rust나 Go로 수천 행에서 수만 행에 달하는 것들은 피커나 뷰어의 UI를 직접 구현하는 경우입니다. 다루는 기능이 하나이며 100행 규모인 본 플러그인에서는 쉘 스크립트로 충분하다는 것을 알 수 있습니다.
구현 규약도 파악할 수 있었습니다. 설정을 가진 플러그인은 HERDR_PLUGIN_CONFIG_DIR 하위에 플러그인 소유의 TOML을 두며, 파일이 없으면 모든 항목이 기본값(default)으로 동작합니다. herdr 호출은 ${HERDR_BIN_PATH:-herdr}의 CLI 서브 프로세스를 사용하는 것이 표준이며, 소켓으로 직접 연결하는 방식은 이벤트 구독(event subscription)이 필요한 구현으로 한정되어 있었습니다.
조사한 구현에는 공통적인 함정도 있었습니다. PATH 해결 실패, pane(패널) 파괴로 인한 자식 프로세스의 종료, pane의 cwd(현재 작업 디렉터리)가 플러그인의 루트와 일치하지 않는 문제 등 세 가지였으며, 앞의 두 가지는 저도 겪었던 문제입니다. pane 파괴로 인한 자식 프로세스의 종료에 대해, Rust 구현인 crafts69guy/herdr-ghq는 Command::process_group(0)과 stdio의 null 연결로 대처하고 있었습니다. process_group(0)은 exec 호출 전에 자식 프로세스에서 setpgid(0, 0)를 호출하여, 자식을 새로운 프로세스 그룹(process group)으로 옮깁니다. hangup의 SIGHUP은 포그라운드 프로세스 그룹으로 전송되기 때문에, 다른 그룹으로 옮겨진 자식에게는 전달되지 않습니다. 저의 trap '' HUP이 "시그널을 무시하여 틈새를 넘어가기" 위한 해법이라면, 이 방식은 "시그널의 목적지에서 벗어나기" 위한 해법으로, 동일한 경합(race condition)에 대한 다른 해결책입니다.
마켓플레이스 공개
git init을 하고 커밋한 뒤, gh repo create kenchan/herdr-ghq-open-agent --public --source . --push 명령어로 공개했습니다. herdr의 마켓플레이스에는 별도의 등록 절차가 없습니다. GitHub의 topic에 herdr-plugin을 추가하기만 하면, 30분마다 진행되는 자동 인덱싱을 통해 게시됩니다.
요약
ghq와 fzf로 Claude를 실행하는 herdr 플러그인을 만들고 마켓플레이스에 공개했습니다. 얻은 배움을 다시 정리합니다.
shell타입 키바인딩은/bin/sh -lc로 실행되며,/etc/profile이 PATH를 덮어쓴다. 바이너리는 주입되는HERDR_BIN_PATH의 절대 경로로 호출한다.- popup은 명령어가 종료되면 닫힌다. 시간이 걸리는 동기 처리(synchronous processing)는 detached(분리된) worker로 분리하고, popup 단계는 즉시 exit(종료)시킨다.
- 부모의 즉시 exit로 인한 PTY(가상 터미널) 파괴는 SIGHUP이 되어, fork()부터
setsid(2)사이의 틈새에 있는 자식 프로세스를 죽인다. spawn 전의trap '' HUP으로 이를 방어한다. - herdr 플러그인 OSS(오픈 소스 소프트웨어)가 충분히 있으므로 먼저 조사하자. 기존의 picker(선택기)를 활용한다면 셸 스크립트가, picker UI를 직접 구현한다면 Rust나 Go 구현이 참고가 된다.
리포지토리는 kenchan/herdr-ghq-open-agent입니다. 동일한 구성의 플러그인을 작성할 때 참고가 된다면 기쁘겠습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기