도구 하나를 급하게 붙일 때 손이 먼저 가는 곳은 함수 시그니처다. 입력 하나, 출력 하나 정하면 다 됐다고 여긴다. 그런데 그 도구를 실제로 에이전트 손에 쥐여 주면 전혀 다른 질문들이 튀어나온다. 모델이 아무 때나 이 도구를 부르지는 않는가. 실행하면 뭐가 바뀌나. 사용자는 지금 뭐가 일어나는지 알 수 있는가. 함수 시그니처만으로는 이 중 하나도 답이 안 나온다.

공개된 Claude Code 도구 동작을 관찰해 일반화한 관점으로 이 감을 다시 확인했다. 한 파일, 한 함수가 아니라 스키마·프롬프트·권한·실행·UI/로그·기능 게이트라는 여섯 조각이 한 세트로 움직인다. 이 중 하나만 빠져도 모델이 제안한 행동과 실제로 실행되는 경계가 흐려진다. 개인 스킬을 만들 때도 똑같은 일을 겪었다. 입력 검증만 해 두고 나머지는 나중에 손보자고 미뤘다가 사용자가 뭐가 실행됐는지 모르는 상태로 한참 남아 있었던 적이 있다. 뒤늦게 하나씩 채워 넣느라 이미 배포한 도구를 다시 뜯어야 했다.

도구는 함수가 아니다

안전한 에이전트 하니스(agent harness, AI가 도구를 스스로 골라 실행하는 실행 틀)에서 도구 하나는 최소 여섯 가지를 동시에 가진다. 이름과 입력 스키마(schema, 입력의 형태를 미리 정해 두는 규격), 언제·왜 써야 하는지 알려주는 프롬프트, 실행 전 허용·거부·확인을 가르는 권한 판단, 실제로 부작용을 일으키는 실행부, 사용자에게 보여줄 진행·결과 화면, 그리고 지금 환경에 그 기능이 있는지 가리는 기능 게이트(feature gate, 조건에 따라 기능을 켜고 끄는 장치)다.

실행 직전에 allow·deny·ask로 갈리는 권한 판정이 명시적으로 내려지지 않으면 나머지 다섯 조각이 아무리 탄탄해도 소용없다. 권한 판정 하나가 뚫리면 스키마가 아무리 촘촘해도 프롬프트가 아무리 정교해도 실행부는 그 구멍을 그대로 통과시킨다. 반대로 권한 판정만 단단하고 나머지 다섯 조각이 허술하면 사용자는 승인은 했는데 실제로 뭐가 실행됐는지 끝내 알 수 없는 상태에 놓인다.

여섯 조각 중 하나라도 약하면 무슨 일이 생기나. 스키마가 헐거우면 모델이 형태가 안 맞는 입력을 넘기고 실행부가 그걸 그대로 받아 처리한다. 프롬프트가 애매하면 모델이 도구를 너무 자주 부르거나 반대로 필요할 때 안 부르며 UI가 부실하면 사용자는 결과 화면만 보고 뭐가 실행됐는지 못 알아챈다. 기능 게이트가 없으면 지금 환경에서 못 쓰는 기능이 목록에는 버젓이 떠 있다. 여섯 조각은 서로 다른 실패를 각자 막는다. 하나가 다른 하나를 대신하지 못한다.

구체적으로 짚어 보자. 파일 하나를 지우는 도구를 새로 만든다면 여섯 조각은 각자 이렇게 움직여야 한다. 스키마는 경로 문자열 하나만 필수값으로 받도록 형태를 가둔다. 그 경로가 실제로 프로젝트 안쪽인지는 스키마만으로는 보장되지 않으며 실행 직전에 경로를 정규화하고 경계를 확인하는 별도 단계에서 가려낼 몫이다. 프롬프트는 이 도구를 되돌릴 수 없는 삭제에만 쓰라고 명시하고 임시 파일 정리 같은 가벼운 용도와 구분해 준다. 권한은 실행 직전에 사용자 확인을 요구하도록 판단하는 자리다. 실행부는 삭제라는 부작용을 실제로 일으키기 전에 무엇이 지워질지 미리 보여주고 그 뒤에야 결과를 만든다. UI는 지워진 파일 목록을 화면에 남기고 기능 게이트는 읽기 전용 환경이면 이 도구 자체를 목록에서 숨긴다. 여섯 조각 중 하나라도 없으면 이 삭제 도구는 어딘가에서 사고를 낼 가능성이 커진다는 뜻이다.

도구 하나를 둘러싼 스키마·프롬프트·권한·실행·UI·기능 게이트 계약 조각들이 체크리스트처럼 겹쳐진 모습을 표현한 일러스트
직접 생성한 오리지널 일러스트. 도구 하나가 안고 있는 여섯 계약을 표현했다.

스키마와 프롬프트: 입력을 가두고 언제 쓸지 알려준다

스키마부터 본다. 도구의 입력 스키마는 모델이 채워 넣을 수 있는 값의 모양을 미리 정해 둔 규격이다. 필드 이름, 타입, 필수 여부, 허용 범위가 여기서 정해진다. 스키마가 헐거우면 모델은 문법적으로는 맞지만 의미로는 말이 안 되는 입력을 만든다. 파일 경로 필드에 아무 문자열이나 받아 주면 존재하지 않는 경로나 상위 디렉터리를 벗어나는 경로까지 그대로 들어오는 셈이다. 스키마는 형식과 허용값 범위를 좁히는 몫이고 실제 경로의 존재·권한·경계는 원래 실행 직전 검증이 맡는 영역이다. 다만 스키마가 형식조차 못 좁히면 실행부가 그 기본 검증까지 떠안아야 하니 코드는 두 배로 늘어난다.

프롬프트는 스키마보다 눈에 덜 띄지만 힘은 더 세다. 도구 설명은 모델에게 문서가 아니라 규칙으로 읽힌다. 언제 이 도구를 써야 하는지, 비슷한 다른 도구와 어떻게 구분되는지, 어떤 입력이 좋은 예시인지가 여기 담기는 자리다. 설명이 너무 짧으면 모델이 도구를 오용하거나 아예 무시한다. 너무 길고 애매하면 모델이 매번 이 도구부터 시도해 불필요한 호출이 쌓인다. 도구 설명은 결국 숨은 제품 정책이나 마찬가지다. 코드 리뷰에서는 곧잘 지나치지만 실제 동작에 미치는 영향은 함수 본문 못지않다.

두 조각을 따로 떼서 생각하면 안 된다. 스키마가 입력의 문법을 정하고 프롬프트가 그 입력을 언제 채워야 하는지 정한다. 스키마만 촘촘하고 프롬프트가 허술하면 모델은 정확한 입력을 엉뚱한 순간에 채워 넣는 게 문제다. 반대로 프롬프트만 정교하고 스키마가 헐거우면 맞는 순간에 틀린 입력을 채운다. 둘 다 맞아야 도구가 제 몫을 한다. 하나만 잘 만들어서는 반쪽짜리 계약이다.

실행: 부작용과 결과를 가른다

실행부에서 가장 먼저 깨야 할 오해가 있다. 셸이나 파일을 다루는 도구는 얇은 래퍼(기존 기능을 그대로 감싸기만 하는 껍데기)가 아니다. 명령을 해석하고 경로를 다루고 격리 여부를 판단하고 되돌릴 수 없는 명령이면 미리 경고하고 출력을 붙잡고 변경 전후를 비교해 보여주는 일이 도구 하나 안에 다 들어간다. 이걸 명령어 하나 그대로 넘기는 얇은 래퍼로 만들면 그 도구는 위험을 사용자에게 그대로 떠넘기는 셈이다. 고위험 기능이라는 표현이 더 정확하다.

실행부의 핵심 원칙은 부작용과 결과를 갈라놓는 것이다. 부작용은 실제로 파일이 바뀌거나 명령이 돌아가거나 외부 상태가 변하는 일이고 결과는 그 부작용을 모델과 사용자에게 요약해 전달하는 값이다. 이 둘을 뒤섞으면 실행이 끝나기도 전에 결과부터 만들어 버리는 사고가 난다. 부작용은 실패했는데 결과는 성공으로 보이는 경우가 여기서 나온다. 실행이 끝난 뒤에야 결과를 만들고 그 결과가 실제로 무엇이 일어났는지 정직하게 담아야 한다. 순서를 지키지 않으면 도구가 거짓말을 하게 된다.

앞서 든 삭제 도구로 돌아가 보자. 실행부가 부작용과 결과를 안 가르면 삭제 명령을 내리자마자 성공 메시지부터 만들어 버릴 수 있다. 실제로는 권한 밖 파일이라 삭제가 막혔는데 결과는 이미 성공으로 표시된 뒤였다면 사용자는 그 파일이 없어진 줄 알고 다음 작업을 이어간다. 부작용을 먼저 확정하고 나서 결과를 만들어야 이런 어긋남이 안 생긴다.

검색이나 조회 도구는 결이 다르다. 저장소나 웹이나 코드 참조 같은 방대한 결과를 그대로 넘기면 컨텍스트가 순식간에 넘친다. 그래서 이런 도구는 결과를 한계가 정해진 맥락(모델에게 넘기기 전에 분량을 잘라내는 범위)으로 줄이는 역할이다. 실행 자체보다 결과를 얼마나 잘 압축하느냐가 도구의 품질을 가른다. 검색 결과를 통째로 던지는 도구는 편해 보여도 다음 턴에서 모델의 판단력을 갉아먹는다.

에이전트나 하위 작업을 부르는 도구는 한 걸음 더 나아간다. 한 번의 실행이 아니라 위임한 작업을 관리하는 일이라 시작·진행·완료라는 생애주기 자체를 도구가 들고 있어야 한다. 실행 하나로 끝나는 도구와 시작만 하고 나중에 상태를 물어야 하는 도구는 UI 계약도 서로 다르다. 이 차이를 무시하고 똑같은 결과 형식을 쓰면 사용자는 아직 안 끝난 작업을 끝난 걸로 착각한다.

도구 호출이 스키마 검증을 거쳐 권한 판정과 실행을 지나 UI 로그로 이어지는 흐름을 나타낸 다이어그램
직접 생성한 오리지널 다이어그램. 도구 호출 한 번이 지나가는 네 단계를 그렸다.

UI·로그와 기능 게이트: 보이는 것과 되는 것을 맞춘다

실행이 끝났다고 도구의 일이 끝나는 게 아니다. 사용자는 진행 중·성공·실패라는 세 가지 상태 중 하나를 화면으로 본다. 이 렌더링이 부실하면 승인이나 디버깅 판단 자체가 어긋나기 마련이다. 무슨 명령이 실행되는지, 어떤 경로가 바뀌는지, 실패했다면 왜 실패했는지가 화면에 없으면 사용자는 도구가 무엇을 했는지 결과가 나온 뒤에도 추측만 한다. UI의 빈틈은 그래서 보기 좋은지의 문제가 아니라 바로 안전의 빈틈으로 이어진다.

질문을 던지거나 계획 모드로 들어가거나 나오는 도구를 보면 이 계약이 왜 중요한지 더 또렷해진다. 이런 도구는 한 번의 응답으로 끝나지 않는 게 특징이다. 사용자에게 질문을 던진 채로 대화가 잠깐 멈추거나 계획 모드로 들어가 평소와 다른 규칙으로 움직인다. 이 상태 전환을 UI가 분명히 보여주지 않으면 사용자는 지금 무슨 모드인지, 아직 해결 안 된 질문이 남아 있는지 모른 채로 다음 지시를 내리기 쉽다. 미결 상태(아직 답하지 않은 채 남은 요청)를 UI가 안 챙기면 그 빚은 조용히 다음 턴으로 넘어간다.

기능 게이트는 마지막 조각이지만 가장 자주 빠뜨린다. 지금 실행 환경에 없는 기능이 도구 목록에는 버젓이 떠 있는 경우다. 특정 통합이 연결 안 됐거나 실행 환경이 그 기능을 지원하지 않으면 그 도구는 애초에 모델 눈에 보이면 안 된다. 안 보이는 게 정답인 도구가 보이면 모델은 존재하지 않는 능력을 시도하고 실패한 뒤에야 그 사실을 안다. 사용자 쪽에서는 방금 그 도구가 왜 실패했는지 이해하기 더 어렵다. 기능 게이트는 이 도구를 쓸 수 있는지를 실행 전에 미리 걸러 내는 조용한 문지기다. 문지기 하나가 빠지면 나머지 다섯 조각이 다 맞아도 사용자는 헛발질을 본다. 환경마다 지원 여부가 갈리는 기능일수록 이 문지기를 도구 목록 앞단에 세워 둬야 실행 단계에서 뒤늦게 막히는 일이 줄어든다.

같은 삭제 도구를 UI 쪽에서 다시 보면 왜 화면이 중요한지가 한층 더 또렷해지는 지점이다. 지워진 파일 목록이 화면에 안 남으면 사용자는 방금 무엇이 사라졌는지 로그를 뒤져야 안다. 읽기 전용 환경에서 이 도구가 목록에 그대로 떠 있으면 사용자는 시도했다가 거부당한 뒤에야 지금 못 쓰는 기능이라는 사실을 알게 되는 셈이다. 미리 안 보이는 것과 시도했다가 막히는 것은 사용자 경험에서 전혀 다르다.

도구 하나를 중심에 두고 스키마·프롬프트·권한·실행·UI로그·기능 게이트라는 여섯 계약이 둘러싼 구조를 나타낸 다이어그램
직접 생성한 오리지널 다이어그램. 도구 하나를 둘러싼 여섯 계약의 구조를 그렸다.

여섯 조각을 한 표로 정리하면 이렇다.

계약 조각 무엇을 정하나 빠지면 생기는 문제
스키마 모델 입력의 형태·필수값·범위 검증 책임이 실행부로 떠밀린다
프롬프트 언제·왜 이 도구를 쓰는지 오용하거나 필요할 때 안 쓴다
권한 실행 전 allow·deny·ask 판단 위험한 실행이 그대로 통과한다
실행 부작용과 결과의 분리 실패했는데 성공으로 보인다
UI·로그 진행·성공·실패 상태 표시 승인·디버깅 판단이 어긋난다
기능 게이트 지금 환경에 그 기능이 있는지 없는 능력을 시도하고 뒤늦게 실패한다

이 표를 도구를 새로 만들 때 체크리스트로 쓴다. 함수 시그니처보다 이 여섯 줄을 먼저 채운다. 스키마와 프롬프트를 정하고 권한 판단 지점을 표시하고 부작용과 결과를 가르고 사용자가 볼 화면을 그리고 마지막으로 이 기능이 지금 환경에 정말 있는지를 확인하는 일이다. 순서를 바꾸면 나중에 같은 자리를 두 번 고치게 된다. 여섯 칸 중 하나라도 빈칸이면 그 도구는 아직 미완성이라고 본다.

allow·deny·ask를 가르는 권한 판정의 세부 라우팅 규칙까지 겹쳐 놓고 보면 이 체크리스트가 왜 여섯 칸에서 멈추는지, 그중 어느 칸이 사고를 가장 크게 막는지가 더 분명해진다. 다음에 도구를 하나 더 붙일 때는 시그니처부터 적는 게 아니다. 이 여섯 줄부터 채운다. 그러고 나서야 함수 본문을 짠다.