티스토리 뷰

Server

[AI] AI Harness(하네스) 구축을 위한 shim 아키텍처 with Busy Box pattern and PATH 하이재킹

망나니개발자 2026. 5. 5. 10:00
반응형



 

1. AI Harness(하네스) 구축을 위한 Shim 아키텍처 


[ shim 아키텍처 개요 ]

"shim"은 원래 목공·기계 조립에서 쓰는 일반 명사로 얇은 쐐기·끼움쇠를 뜻한다. 두 부품 사이의 미세한 틈을 메우거나 높이를 맞추기 위해 끼워 넣는 얇은 조각(나무, 금속, 플라스틱)으로, 문 경첩이 안 맞을 때 종이를 접어서 끼우는 그 동작이 바로 shim이다.

마이크로소프트는 이러한 어원에 착안하여 windows 호환성 레이어를 위한 구조를 잡는 데 Shim 이라는 용어를 처음 사용하며 널리 알려지게 되었다. Windows의 Application Compatibility Toolkit (ACT) 문서에서 Microsoft는 이렇게 정의한다.

A shim is a small library that transparently intercepts an API, changes the parameters passed, handles the operation itself, or redirects the operation elsewhere. (얇은 라이브러리로서 API 호출을 투명하게 가로채 인자를 바꾸거나, 동작을 직접 처리하거나, 다른 곳으로 리다이렉트한다.)

 

 

물리적 끼움쇠가 두 부품 사이에 끼어 틈을 메우듯, 소프트웨어에서 shim은 호출자와 피호출자 사이에 개입하여 호환성·동작 변경·계측을 수행하는 코드를 가리킨다. shim과 헷갈리기 쉬운 용어가 몇 개 있는데, 정리하면 이렇다.

  • wrapper: 더 일반적인 용어. 다른 함수/명령을 감싸는 모든 코드. shim은 wrapper의 한 종류로, 투명성·호환성에 초점이 있음
  • adapter: 인터페이스가 다른 두 구성요소를 이어주는 코드. shim은 보통 같은 인터페이스를 유지한 채 동작만 바꿈
  • proxy: 호출을 대신 받아서 처리하거나 전달하는 코드. proxy는 보통 호출자가 의식적으로 거치지만, shim은 호출자가 모르는 사이에 끼어듦

 

 

[ AI Harness(하네스)와 shim 아키텍처 ]

그렇다면 AI 하네스와 shim 아키텍처는 어떠한 연관성을 가질 수 있을까? Claude.md부터 시작해서 skill과 hook, agent.md 등 AI 하네스를 위한 도구들은 모두 우리의 로컬 PC에 설치되는 온디바이스(On-device) 성향이다. 우리가 코드를 리팩토링하고 유지보수하듯이, 이러한 도구들 역시 그 대상이다. 하지만 문제는 이들은 한 번 설치하면 새롭게 업데이트하는 경우가 드물다는 것이다. 우리가 claude나 gemini, codex를 이용하면서, 설치한 스킬들의 신규 버전을 패치 받는다면 매우 용이할 것이다. 특히나 이들은 .md 형태의 파일이기 때문에 버그의 가능성도 현저히 낮다.

이 과정에서 우리는 shim 아키텍처를 사용할 수 있다. 중간에 AI Harness를 신규 버전으로 동기화시키고, 필요한 작업들을 수행하는 shim을 끼워넣는다면 이러한 이점을 누릴 수 있다.

 

 

또한 shim 아키텍처를 통해 사용자에게 비침투적으로 접근할 수 있다. 만약 shim이 없다면, 새로운 도구의 존재와 이름을 인식하고 별도 명령을 써야 할 수 있다. 가령 우리가 만든 하네스의 이름이 MAHA(MAngkyu HArness) 라고 하자. 그렇다면 기존에 익숙하던 claude로 실행하는 것이 아닌, maha 혹은 maha-claude 와같은 별도 명령을 쓰게 만들면서 진입 장벽이 생길 수 있다. shim 아키텍처는 이러한 비파괴성의 원칙 역시 준수할 수 있다.

얇고(단일 바이너리, 빠른 실행), 투명하게 가로채고(개발자는 평소처럼 claude만 친다), 동작을 추가하고(harness 주입), 진짜 Claude Code와 같은 인터페이스를 유지한다. 그래서 이를 "shim 아키텍처"라고 부른다.

 

 

 

2. Shim 아키텍처 내부 구현 


[ 가짜 바이너리 ]

위의 그림에서 보이듯, 우리가 claude를 입력했을 때, claude와 같은 진짜 코딩 에이전트가 실행되는 것이 아니라, 중간에 우리의  요구사항을 처리할 가짜 바이너리(가짜 claude)이 필요하다. 가짜 바이너리 claude는 다음의 역할을 가질 수 있으며, 물론 이는 구축하는 하네스에 따라 달라질 것이다.

  1. 팀 공통 harness(스킬·훅·규칙)를 git에서 sync
  2. 그 내용을 ~/.claude/ 와 같은 에이전트 설정에 반영
  3. 텔레메트리 환경변수 주입
  4. 진짜 claude로 프로세스를 교체하고 종료됨

 

이 과정이 100ms 이내에 끝나야 개발자가 "느려졌다"고 느끼지 않으므로, Go 단일 바이너리로 만드는 것을 권장한다.

또한 팀 공통 하네스를 특정 경로에 sync 받고, 이를 ~/.claude/skils 등에 심볼릭 링크로 생성하면 공통으로 적용할 수 있다.

그리고 독립된 디렉토리가 아닌, 공용으로 사용되는 단일 파일인 settings.json에도 우리의 하네스 설정이 추가될 수 있다. 이를 위해 "__our_shim": true 와 같은 마커 내용을 추가로 넣어, 설치와 삭제 시에 자기가 추가한 것만 골라내 제거시키고 개발자가 직접 적은 한 줄도 안 건드리게 설계할 수 있다.

 

 

[ BusyBox 패턴 ]

일반적으로 사람들은 자기 입맛에 맞는 코딩 에이전트를 활용한다. 따라서 우리는 claude 뿐만 아니라 gemini와 codex까지 대응되는 AI 하네스를 만들어야 하는데, 그렇다면 가짜 바이너리를 매번 새롭게 만드는 것은 너무 번거롭다. 이때 Busybox 패턴을 활용하면 이 문제를 쉽게 해결할 수 있다. BusyBox 패턴이란 하나의 실행 파일이 자신의 호출 이름(argv[0])을 보고 여러 가지 다른 명령으로 동작하는 패턴이다. 심링크 N개가 모두 동일한 단일 바이너리를 가리키고, 그 바이너리는 자기가 어떤 이름으로 호출됐는지 확인해서 동작을 분기한다.

이 패턴은 busybox라는 실제 리눅스 도구에서 이름이 왔는데, 리눅스에는 ls, cat, cp, mv, grep, wc, tar, vi, sh 등 수백 개의 유닉스 명령어가 존재한다. 이를 새로운 바이너리로 매번 만들려면 번거롭기 때문에 busybox라는 단 하나의 실행 파일(약 1MB)에 수백 개의 유닉스 명령어를 전부 박아 넣었다.

/bin/ls    → /bin/busybox  (심링크)
/bin/cat   → /bin/busybox  (심링크)
/bin/cp    → /bin/busybox  (심링크)
/bin/grep  → /bin/busybox  (심링크)

 

 

핵심 메커니즘은 argv[0] 이다. 모든 프로그램은 시작할 때 자기가 어떤 이름으로 호출됐는지 알 수 있다. argv[0](첫 번째 인자)에 그 이름이 들어있기 때문이다. 따라서 우리가 원하는 명령어으로 손쉽게 분기할 수 있다. 이를 통해 임베디드 리눅스나 도커베이스 이미지처럼 디스크가 작은 환경에서 이 도구 하나만 설치하면 사실상 모든 기본 명령어를 다 쓸 수 있게 해두었다. 또한 디스크를 절약하고 공용 라이브러리 추출 작업 생략, 배포 단순화 등의 많은 이점을 누릴 수 있다.

 

 

 

Shim 아키텍처에도 동일한 패턴을 적용할 수 있다.

~/.shim/bin/claude  → ~/.shim/bin/shim  (심링크)
~/.shim/bin/codex   → ~/.shim/bin/shim  (심링크)
~/.shim/bin/gemini  → ~/.shim/bin/shim  (심링크)

 

 

3개 심링크가 모두 단일 shim 바이너리로 향한다. shim 안의 main은 argv[0]을 보고 분기한다.

func main() {
    switch filepath.Base(os.Args[0]) {
        case "claude", "codex", "gemini":
            shim.Run(name)  // shim 모드: harness 적용 + 진짜 바이너리로 exec
        default:
            runCLI()         // CLI 모드: init/doctor/sync 등 자체 명령
        }
}

 

 

이를 그림으로 표현하면 다음과 같다.

 

 

 

[ Path 하이재킹(PATH Hijacking) ]

이제 claude라는 명령이 실제 claude가 아닌 가짜 가짜 shim 바이너리를 실행시키기만 하면 된다. 이 과정에서는 Path 하이재킹(PATH Hijacking) 기법을 이용할 수 있다.

PATH 환경변수란 셸이 명령어를 찾을 때 뒤지는 디렉토리 목록을 담은 환경변수다. 터미널에서 claude를 치면 셸은 다음과 같은 흐름으로 동작한다.

  1. PATH에 들어있는 디렉토리들을 왼쪽부터 순서대로 훑는다
  2. 각 디렉토리 안에서 claude라는 실행 파일을 찾는다
  3. 첫 번째로 발견되는 파일을 실행한다

 

echo $PATH 를 치면 자기 PC의 실제 PATH가 콜론으로 구분돼 출력된다. 또한 which 명령어를 활용하면, claude 같은 명령어가 실제로 어떤 파일로 실행되는지 확인 가능하다.

$ which claude
/Users/mangkyu/bin/claude

 

 

이 말을 뒤집어보면, 가짜 실행 파일을 만들어 PATH 맨 앞에 끼워 넣어 두면 OS가 가짜 쪽을 먼저 잡게 됨을 뜻하며, 이것을 바로 PATH 하이재킹(PATH Hijacking) 이라 부른다. 우리가 만든 가짜 바이너리를 PATH의 가장 앞에 넣어 shim 아키텍처를 완성할 수 있다.

 

 

[ 프로세스 교체(syscall.Exec) ]

남은 마무리는 이제 가짜 바이너리에서 처리를 끝내고 실제 바이너리를 호출하는 것이다. 보통 프로그램은 fork+exec로 자식 프로세스를 만드는데, 이러면 부모 shim이 살아남아 ps에 잡히고 메모리도 차지한다. 하지만 syscall.Exec 와 같이 현재 프로세스를 다른 프로그램으로 "교체"하는 시스템 콜을 활용하면, 새 프로세스를 만들지 않고 기존 프로세스를 대체시킬 수 있다. 이를 통해 다음의 이점을 누릴 수 있다.

  • PID가 바뀌지 않음 (부모-자식 관계가 없음)
  • ps에 shim 프로세스가 남지 않음
  • 메모리에 shim 코드가 남지 않음
// internal/shim/exec_unix.go
func execBinary(path string, args []string, env []string) error {
    return syscall.Exec(path, args, env)
}

 

 

개발자가 ps로 프로세스 목록을 봐도 shim의 흔적이 없다. 진짜 claude만 보인다. 투명성이라는 설계 원칙은 이 한 줄에 압축돼 있다.

 

[ 최종 실행 흐름 ]

이제 우리의 shim 아키텍처를 적용한 상태에서 개발자가 claude를 치면 다음과 같은 일이 일어난다. 하지만 그럼에도 불구하고 개발자에게는 평소와 동일한 Claude Code가 보이고 실행된다.

  1. 개발자가 claude 입력
  2. OS가 PATH에서 ~/.shim/bin/claude 발견 (심링크 → shim)
  3. shim 바이너리 실행, argv[0] = "claude"
  4. main()이 binName="claude"를 보고 shim.Run("claude") 진입
  5. 이후 하네스 동기화 작업 진행
    1. harness 동기화 (git sync, 캐시 우선 → 네트워크 fallback)
    2. 에이전트 설정 적용 (스킬 심링크, 훅 머지, 규칙 @import)
    3. 텔레메트리 환경변수 주입 (claude만)
    4. syscall.Exec → 진짜 claude로 프로세스 교체

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함