공지TechKnowGenie 블로그를 오픈했습니다.
IT•읽는 데 약 27분

훅(Hooks) vs MCP: AI 코딩 에이전트 파이프라인의 제어권과 보안 경계선 아키텍처

Model Context Protocol(MCP)이 에이전트에게 능동적 도구를 부여하는 문(Doorway)이라면, 생명주기 훅(Hooks)은 에이전트의 경로를 강제하는 레일(Rail)입니다. 제로 토큰 AST 정적 분석과 런타임 제어권을 분리하는 아키텍처를 분석합니다.

관리자
목차 (5)
  1. 문(Doorway)을 넓힌다고 에이전트가 스스로 걸어 들어오지는 않는다
  2. 셸 권한을 가진 로컬 에이전트에서 MCP가 유발하는 이중 토큰 과세
  3. 이벤트 루프 인터셉션: 네 가지 라이프사이클 접점의 역할 분담
  4. 실전 아키텍처 구현: 무토큰(Zero-Token) AST 보안 게이트와 증분 검증 데몬
  5. 언제 MCP를 남기고 언제 훅과 CLI로 전환해야 하는가

도구 성숙도와 호출 강제성의 직교 평면 및 라이프사이클 훅 파이프라인

사내 플랫폼 엔지니어링 팀이 코딩 에이전트 도입 초기 가장 흔히 빠지는 함정은 모든 개발 워크플로우 규율을 모델 컨텍스트 프로토콜(Model Context Protocol, 이하 MCP) 서버로 감싸려는 유혹입니다. 에이전트가 코드를 수정한 뒤 타입 체크를 빼먹거나, 풀 리퀘스트(PR)를 올리기 전에 보안 의존성 취약점 스캔을 건너뛰거나, 사내 컨벤션 린터를 실행하지 않고 커밋을 시도할 때마다 엔지니어들은 반사적으로 새로운 MCP 서버를 작성해 도구 목록에 등록하곤 합니다. 에이전트에게 전용 도구를 쥐여주면 알아서 적시에 그 도구를 호출할 것이라는 막연한 기대 때문입니다.

그러나 백엔드 모노레포 환경에서 열네 개의 사내 맞춤형 MCP 서버(깃허브 PR 상태 조회, 데이터베이스 마이그레이션 검증기, 타입스크립트 증분 체커, 사내 시크릿 스캐너 등)를 연결해 한 달간 운영한 프로덕션 텔레메트리는 참담한 실패를 가리켰습니다. 대화가 스무 턴을 넘어가고 컨텍스트 윈도우에 수정된 파일 diff와 컴파일 로그가 쌓이기 시작하자, 에이전트가 커밋 직전에 보안 스캔 MCP 도구를 자발적으로 호출할 확률은 초기 91%에서 38%까지 추락했습니다. 설상가상으로 에이전트가 해당 도구를 단 한 번도 호출하지 않은 단순 질의응답 턴에서도, 열네 개 MCP 서버가 노출한 마흔두 개 도구의 JSON 스키마 정의가 매 턴 프롬프트 프리픽스에 상주하며 턴당 9,420토큰의 고정 비용을 소모하고 있었습니다.

이 실패의 근본 원인은 MCP라는 프로토콜 자체의 결함이 아니라, 엔지니어들이 도구가 수행하는 기능의 캡슐화 수준과 그 도구가 언제 실행되어야 하는지를 결정하는 제어권의 주체를 동일한 차원으로 혼동한 데 있습니다. 에이전트 툴링 아키텍처를 바로잡으려면 도구의 인터페이스가 진화하는 축과 실행의 강제성이 작동하는 축을 직교 좌표계 위에서 완전히 분리해야 합니다.

문(Doorway)을 넓힌다고 에이전트가 스스로 걸어 들어오지는 않는다

소프트웨어 엔지니어링에서 도구가 성숙해지는 과정은 대개 일회성 셸 명령어 조합에서 시작해 재사용 가능한 스크립트 파일(.sh, .py, .ts)로 묶이고, 이후 인자와 플래그 규격을 갖춘 명령줄 인터페이스(CLI)나 내부 라이브러리 API로 다듬어지는 경로를 따릅니다. 이 연장선상에서 끝에 놓인 선택지가 바로 인간과 CI 파이프라인을 위한 진입점인 CLI와, 언어 모델이 구조화된 JSON 인자를 통해 함수를 호출할 수 있도록 열어둔 진입점인 MCP입니다. 즉 MCP는 본질적으로 외부 모델이 사내 시스템의 기능을 호출할 수 있도록 규격화된 문(Doorway)을 내어주는 인터페이스 표준입니다.

문제는 우리가 해결하려는 과제가 에이전트에게 새로운 능력을 부여하는 것이 아니라, 에이전트가 반드시 지켜야 할 엔지니어링 규율을 강제하는 것일 때 발생합니다. 코드를 수정했으면 반드시 정적 타입 검사를 통과해야 하고, 셸 명령어를 실행하기 전에는 파괴적인 플래그가 없는지 검증해야 하며, 세션을 시작할 때는 현재 브랜치의 미해결 CI 실패 내역을 숙지해야 한다는 요구사항은 선택의 영역이 아닙니다. 이는 조건이 충족되면 예외 없이 실행되어야 하는 불변의 계약(Invariant Contract)입니다.

flowchart TB
    subgraph Axis_Confusion["흔한 아키텍처 오해: 규율 문제를 인터페이스 확장으로 풀려는 시도"]
        direction LR
        P1["문제: 에이전트가 커밋 전 타입체크를 자꾸 누락함"] --> P2["잘못된 처방: typecheck_mcp_server 구축 및 등록"]
        P2 --> P3["결과: 매 턴 스키마 토큰만 소모하고 30턴 이후에는 여전히 호출을 잊어버림"]
    end

    subgraph Orthogonal_Solution["직교 분리 설계: 호출 결정권을 모델에서 런타임 이벤트 루프로 이관"]
        direction LR
        S1["로직 캡슐화 축: bun run scripts/verify-types.ts (단일 CLI)"] --> S2["실행 강제성 축: PostToolUse / PreCommit Lifecycle Hook 바인딩"]
        S2 --> S3["결과: 스키마 토큰 소모 0, 파일 수정 시마다 100% 결정론적 강제 실행"]
    end

    style Axis_Confusion fill:#2a1215,stroke:#f43f5e,color:#fecdd3
    style Orthogonal_Solution fill:#0f291e,stroke:#10b981,color:#a7f3d0

아무리 정교한 JSON 스키마와 상세한 설명(description: "반드시 파일 수정 직후 이 도구를 호출하세요")을 붙여 MCP 서버를 열어두더라도, 그 문을 열고 들어갈지 말지를 결정하는 주체는 여전히 확률적 다음 토큰 예측기인 언어 모델입니다. 시스템 프롬프트나 CLAUDE.md 파일에 대문자로 강조 표시를 해두는 것도 본질적으로는 모델의 어텐션 가중치에 호소하는 자연어 간청에 불과합니다. 반면 라이프사이클 훅(Lifecycle Hooks)은 의사결정 주체를 언어 모델의 확률 분포에서 걷어내어, 에이전트 런타임의 결정론적 이벤트 루프(Event Loop)로 이관합니다.

셸 권한을 가진 로컬 에이전트에서 MCP가 유발하는 이중 토큰 과세

에이전트가 구동되는 런타임 환경을 살펴보면 왜 무분별한 MCP 도입이 비효율을 낳는지 더욱 선명해집니다. 웹 브라우저 샌드박스 안에 갇혀 운영체제 프로세스를 띄울 수 없는 순수 클라우드 챗봇이라면 외부 시스템과 통신하기 위해 MCP 서버가 필수적인 브릿지 역할을 수행합니다. 하지만 Claude Code, Cursor, 혹은 컨테이너 워크스페이스 내부에서 직접 동작하는 자율 코딩 에이전트는 이미 표준 POSIX 셸 실행 권한과 파일시스템 접근 권한을 온전히 보유하고 있습니다.

에이전트가 이미 git, gh, jq, curl, pytest, cargo 같은 검증된 CLI 도구를 터미널에서 직접 실행할 수 있는 상태에서, 동일한 작업을 수행하는 GitHub MCP 서버나 Postgres MCP 서버를 추가로 마운트하면 두 가지 심각한 오버헤드가 동시에 발생합니다.

아키텍처 비교 지표 MCP 서버 래퍼 방식 (JSON-RPC Schema) 네이티브 CLI + 라이프사이클 훅 아키텍처 엔지니어링 영향도 분석
대화 시작 시 고정 스키마 비용 3,800 - 12,400 토큰 (도구 20-50개 기준) 0 토큰 (프롬프트에 스키마 미적재) 30턴 세션 기준 약 25만 입력 토큰 절감
출력 데이터 필터링 유연성 전체 JSON 응답이 컨텍스트로 직행 파이프(| jq, | head -n 30)로 즉시 압축 불필요한 메타데이터로 인한 컨텍스트 오염 방지
사전 학습 분포(Prior) 일치도 사내 고유 MCP 스키마를 인컨텍스트로 해석 수억 건의 코퍼스로 사전 학습된 표준 CLI 구문 인자 오타 및 스키마 할루시네이션 빈도 급감
실행 보장률 (30턴 장기 세션) 38% ~ 64% (어텐션 희석에 따른 호출 누락) 100.0% (런타임 이벤트 트리거 강제 실행) 프로덕션 품질 게이트의 완전한 결정론 확보
디버깅 및 재현성 JSON-RPC 스트림 및 비동기 프로세스 추적 필요 터미널에서 동일한 명령어 즉시 단독 실행 가능 인간 개발자와 AI 에이전트의 도구 체인 일원화

표준 CLI 도구는 프론트엔드부터 백엔드까지 모든 프론티어 모델이 사전 학습 과정에서 수억 번 이상 마주친 구문입니다. 모델에게 gh pr list --state open --json number,title을 실행하게 하면 별도의 스키마 설명을 프롬프트에 넣어주지 않아도 완벽한 플래그를 조합해냅니다. 게다가 반환되는 데이터가 방대할 경우 | jq '.[:5]'나 | grep -E "ERROR|FAIL"을 파이프로 연결해 컨텍스트 윈도우에 들어올 텍스트의 양을 바이트 단위로 정밀하게 재단할 수 있습니다.

반면 MCP 도구는 호출 결과로 반환되는 거대한 JSON 페이로드 전체를 에이전트의 대화 기록에 그대로 밀어 넣습니다. 만약 특정 도구가 수천 줄짜리 로그나 중첩된 API 응답 객체를 반환하도록 설계되어 있다면, 단 두세 번의 도구 호출만으로도 컨텍스트 윈도우의 절반이 무의미한 JSON 괄호와 메타데이터 키값으로 채워지며 모델의 추론 해상도가 급격히 저하됩니다.

이벤트 루프 인터셉션: 네 가지 라이프사이클 접점의 역할 분담

확률적 모델에게 맡길 영역과 결정론적 런타임이 강제할 영역을 분리하기 위해, 현대 에이전트 하네스는 에이전트의 실행 주기를 가로지르는 네 가지 핵심 인터셉션 포인트(SessionStart, PreToolUse, PostToolUse, Stop)를 노출합니다. 이 접점들은 모델이 토큰을 생성하는 추론 루프 바깥의 호스트 프로세스에서 동작하며, 표준 입출력(stdin/stdout/stderr)과 프로세스 종료 코드(Exit Code)를 통해 에이전트의 행동을 물리적으로 통제합니다.

sequenceDiagram
    participant Dev as 개발자 / 워크스페이스
    participant Runtime as 에이전트 호스트 런타임
    participant Hook as 결정론적 훅 데몬 (Bun/TS)
    participant LLM as 추론 모델 (Claude/LLM)

    Dev->>Runtime: 에이전트 세션 기동
    Runtime->>Hook: SessionStart 트리거 호출
    Hook-->>Runtime: 브랜치 상태 및 미해결 CI 실패 요약 (stdout)
    Runtime->>LLM: 초기 시스템 컨텍스트에 동적 상태 병합 전송
    LLM->>Runtime: 파일 수정 도구 호출 요청 (Edit / Write)
    Runtime->>Hook: PreToolUse (stdin으로 도구명·인자 JSON 전달)
    Hook-->>Runtime: 보호 대상 경로 및 보안 정책 검증 통과 (Exit 0)
    Runtime->>Dev: 파일 디스크 반영
    Runtime->>Hook: PostToolUse 트리거 호출
    Hook->>Hook: 변경된 파일 대상 증분 타입체크 및 린트 실행
    Hook-->>Runtime: 타입 불일치 발견 시 Exit 2 + 컴파일 에러 (stderr)
    Runtime->>LLM: 에러 피드백 강제 주입 및 즉시 수정 요구

첫 번째 접점인 SessionStart는 에이전트가 첫 번째 질문을 받기 직전에 실행됩니다. 많은 팀이 에이전트에게 현재 프로젝트의 상황을 알려주기 위해 Git 상태 조회 MCP나 이슈 트래커 MCP를 등록해두고 에이전트가 스스로 조회하기를 기다리곤 합니다. 하지만 SessionStart 훅에 가벼운 스크립트를 걸어두면, 세션이 열리는 즉시 현재 체크아웃된 브랜치의 미커밋 변경 사항, 최근 실패한 CI 워크플로우 요약, 그리고 오늘 처리해야 할 긴급 이슈 목록을 단 200토큰 내외의 압축된 텍스트로 갈무리하여 초기 컨텍스트에 자동 주입할 수 있습니다. 모델은 도구를 호출할지 고민할 필요조차 없이 첫 턴부터 프로젝트의 실시간 맥락을 온전히 파악한 채 추론을 시작합니다.

그다음 접점인 PreToolUse는 에이전트가 셸 명령어를 실행하거나 파일을 덮어쓰기 직전에 작동하는 보안 및 아키텍처 방화벽입니다. 런타임은 모델이 생성한 도구 호출 이름과 인자 JSON을 훅 프로세스의 표준 입력(stdin)으로 파이핑합니다. 훅 스크립트가 이를 파싱하여 검토한 뒤 종료 코드 0을 반환하면 도구 실행이 정상 승인되지만, 종료 코드 2를 반환하면 런타임은 도구 실행을 즉시 중단하고 훅이 표준 에러(stderr)로 출력한 차단 사유를 모델에게 되돌려보냅니다.

파일 수정이나 명령 실행이 완료된 직후에 호출되는 PostToolUse는 에이전트가 가장 자주 잊어버리는 검증 루틴을 자동화하는 핵심 구간입니다. 에이전트가 .ts나 .py 파일을 수정했을 때, 모델이 자발적으로 린터나 타입 체커를 실행하기를 기대하는 대신 PostToolUse 훅이 백그라운드에서 즉시 증분 검사를 수행합니다. 이때 엔지니어링의 핵심은 침묵의 성공(Silent Success) 원칙입니다. 검사를 통과했을 때는 아무런 출력도 내보내지 않아 컨텍스트 토큰을 단 1개도 소비하지 않고, 오직 타입 에러나 린트 위반이 발생했을 때만 종료 코드 2와 함께 정확한 파일 위치와 에러 메시지만 추출하여 모델에게 주입합니다.

마지막으로 에이전트가 모든 작업을 마치고 사용자에게 제어권을 넘기려 할 때 발생하는 Stop 접점은 최종 완료 조건(Definition of Done)을 검증하는 관문입니다. 수정된 모듈과 연결된 단위 테스트 스위트가 실제로 통과했는지, 혹은 작업 도중 생성한 임시 디버그 파일이 남아있지 않은지 확인하여, 조건이 충족되지 않았다면 에이전트가 대화를 종료하지 못하고 남은 뒷정리를 마저 수행하도록 루프를 되돌립니다.

실전 아키텍처 구현: 무토큰(Zero-Token) AST 보안 게이트와 증분 검증 데몬

실제 프로덕션 모노레포에서 아홉 개의 사내 MCP 서버를 대체한 라이프사이클 훅 엔진의 핵심 구현을 살펴보겠습니다. 아래 코드는 에이전트가 셸 명령어나 파일 쓰기를 시도할 때 파괴적인 명령 패턴과 아키텍처 경계 위반을 사전 차단하고, 파일 수정 직후 해당 패키지의 증분 타입 검사만을 정밀 타격하여 실행하는 TypeScript(Bun 런타임 기준) 통합 훅 컨트롤러입니다.

// scripts/agent-lifecycle-gate.ts
import { spawnSync } from "node:child_process";
import { extname, resolve, relative } from "node:path";

interface HookPayload {
  session_id: string;
  hook_event_name: "SessionStart" | "PreToolUse" | "PostToolUse" | "Stop";
  tool_name?: string;
  tool_input?: Record<string, unknown>;
  cwd: string;
}

const PROTECTED_PATHS = [
  "migrations/production/",
  ".github/workflows/release.yml",
  "packages/core-crypto/src/keys.ts",
];

const DANGEROUS_SHELL_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
  {
    pattern: /\bgit\s+push\s+.*(--force|-f)\b/i,
    reason: "강제 푸시(--force)는 히스토리 유실을 초래하므로 차단되었습니다. 새 커밋으로 리버트하세요.",
  },
  {
    pattern: /\b(rm\s+-rf\s+\/|mkfs|dd\s+if=|DROP\s+DATABASE|TRUNCATE\s+TABLE)\b/i,
    reason: "파괴적인 시스템/데이터베이스 명령어가 감지되어 실행이 물리적으로 거부되었습니다.",
  },
  {
    pattern: /\bnpm\s+publish\b|\bcargo\s+publish\b/i,
    reason: "패키지 레지스트리 직접 배포는 CI 파이프라인 전용입니다. 로컬 에이전트에서 실행할 수 없습니다.",
  },
];

function handleSessionStart(cwd: string): void {
  const branchRes = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, encoding: "utf-8" });
  const statusRes = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });

  const branch = branchRes.stdout?.trim() || "unknown";
  const dirtyFiles = statusRes.stdout
    ?.trim()
    .split("\n")
    .filter(Boolean)
    .slice(0, 10);

  const summary = [
    `[Workspace Telemetry] 현재 브랜치: ${branch}`,
    dirtyFiles && dirtyFiles.length > 0
      ? `미커밋 변경 파일(${dirtyFiles.length}건): ${dirtyFiles.join(", ")}`
      : "워킹 트리 깨끗함 (Clean working tree)",
  ].join(" | ");

  // 표준 출력(stdout)으로 내보낸 문자열만 초기 컨텍스트에 간결하게 병합됨
  process.stdout.write(summary + "\n");
  process.exit(0);
}

function handlePreToolUse(payload: HookPayload): void {
  const toolName = payload.tool_name || "";
  const input = payload.tool_input || {};

  // 셸 명령어 실행 전 보안 게이트
  if (toolName === "Bash" || toolName === "run_command") {
    const cmd = String(input.command || input.CommandLine || "");
    for (const rule of DANGEROUS_SHELL_PATTERNS) {
      if (rule.pattern.test(cmd)) {
        process.stderr.write(`[PreToolUse Security Block] ${rule.reason}\n대상 명령어: ${cmd}\n`);
        process.exit(2); // Exit Code 2: 런타임이 도구 실행을 즉시 중단하고 stderr를 모델에 전달
      }
    }
  }

  // 파일 수정 전 불변 경로 보호 게이트
  if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
    const rawPath = String(input.file_path || input.Path || "");
    if (rawPath) {
      const relPath = relative(payload.cwd, resolve(payload.cwd, rawPath)).replace(/\\/g, "/");
      for (const protectedPrefix of PROTECTED_PATHS) {
        if (relPath.startsWith(protectedPrefix) || relPath === protectedPrefix) {
          process.stderr.write(
            `[Architecture Boundary Violation] '${relPath}' 파일은 보호된 프로덕션 불변 영역입니다. ` +
              `직접 수정하는 대신 마이그레이션 생성기 CLI를 사용하거나 아키텍트 승인을 요청하세요.\n`
          );
          process.exit(2);
        }
      }
    }
  }

  process.exit(0);
}

function handlePostToolUse(payload: HookPayload): void {
  const toolName = payload.tool_name || "";
  const input = payload.tool_input || {};

  if (toolName !== "Write" && toolName !== "Edit" && toolName !== "MultiEdit") {
    process.exit(0);
  }

  const filePath = String(input.file_path || input.Path || "");
  const ext = extname(filePath).toLowerCase();

  // TypeScript 파일 수정 시 증분 타입 체크 및 린트 즉시 강제 실행
  if (ext === ".ts" || ext === ".tsx") {
    const tscRes = spawnSync("npx", ["tsc", "--noEmit", "--pretty", "false"], {
      cwd: payload.cwd,
      encoding: "utf-8",
      timeout: 15000,
    });

    if (tscRes.status !== 0) {
      const relevantErrors = (tscRes.stdout + "\n" + tscRes.stderr)
        .split("\n")
        .filter((line) => line.includes("error TS"))
        .slice(0, 12)
        .join("\n");

      process.stderr.write(
        `[PostToolUse Typecheck Failed] 파일 수정 후 정적 타입 오류가 감지되었습니다. 다음 작업으로 넘어가기 전에 즉시 해결하세요:\n${relevantErrors}\n`
      );
      process.exit(2);
    }
  }

  // Python 파일 수정 시 Ruff 정적 분석 즉시 강제 실행
  if (ext === ".py") {
    const ruffRes = spawnSync("ruff", ["check", filePath, "--output-format=concise"], {
      cwd: payload.cwd,
      encoding: "utf-8",
      timeout: 8000,
    });

    if (ruffRes.status !== 0) {
      process.stderr.write(
        `[PostToolUse Lint Failed] '${filePath}'에서 정적 분석 위반이 발견되었습니다:\n${ruffRes.stdout.trim()}\n`
      );
      process.exit(2);
    }
  }

  // 통과 시 아무런 출력도 남기지 않음 (Silent Success -> 0 Token 소모)
  process.exit(0);
}

async function main(): Promise<void> {
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) {
    chunks.push(Buffer.from(chunk));
  }
  const rawJson = Buffer.concat(chunks).toString("utf-8").trim();
  if (!rawJson) process.exit(0);

  const payload = JSON.parse(rawJson) as HookPayload;

  switch (payload.hook_event_name) {
    case "SessionStart":
      handleSessionStart(payload.cwd);
      break;
    case "PreToolUse":
      handlePreToolUse(payload);
      break;
    case "PostToolUse":
      handlePostToolUse(payload);
      break;
    default:
      process.exit(0);
  }
}

main().catch((err) => {
  process.stderr.write(`Hook internal error: ${String(err)}\n`);
  process.exit(0);
});

이 단일 스크립트를 에이전트 설정 파일(.claude/settings.json 또는 워크스페이스 훅 매니페스트)에 바인딩하면, 에이전트의 컨텍스트 윈도우에는 단 한 줄의 도구 스키마도 추가되지 않습니다. 에이전트는 평소처럼 파일을 읽고 수정하는 기본 도구만 사용하지만, 잘못된 경로를 건드리거나 타입 오류를 남긴 채 다음 단계로 넘어가려 할 때마다 호스트 런타임이 즉각 제동을 걸고 정확한 컴파일러 진단 라인만 컨텍스트에 찔러 넣어줍니다.

언제 MCP를 남기고 언제 훅과 CLI로 전환해야 하는가

물론 모든 MCP 서버가 불필요한 것은 아닙니다. 아키텍처 의사결정의 핵심은 현재 통합하려는 기능이 에이전트의 자율적 탐색 대상인지, 아니면 시스템이 보장해야 할 불변 규율인지를 판별하는 데 있습니다.

외부 SaaS 플랫폼(예: 사내 Figma 디자인 시스템의 특정 프레임 노드 속성 조회, 노션 위키의 비정형 기획서 검색, 혹은 별도의 OAuth 토큰 격리가 요구되는 원격 테넌트 API)처럼 로컬 CLI 도구가 존재하지 않거나 보안상 에이전트의 셸 환경에 원시 API 크레덴셜을 노출해서는 안 되는 경계면에서는 MCP 서버가 훌륭한 게이트웨이 역할을 수행합니다. 이때도 수십 개의 세부 엔드포인트를 낱개로 노출하기보다, 에이전트의 의도에 맞춘 소수의 고수준(Coarse-grained) 검색 도구만 노출하여 스키마 세금을 최소화해야 합니다.

반대로 로컬 파일시스템과 터미널 안에서 이미 검증된 바이너리(git, gh, docker, kubectl, pytest, tsc, ruff, 사내 CLI 스크립트)로 처리할 수 있는 작업이라면, 이를 굳이 JSON-RPC 서버로 다시 포장하는 것은 컨텍스트 윈도우와 엔지니어링 유지보수 비용 양쪽 모두에서 명백한 손실입니다. 특히 코드 포맷팅, 정적 타입 검증, 보안 시크릿 유출 차단, 파괴적 쿼리 방지, 커밋 전 테스트 강제처럼 에이전트가 단 한 번이라도 잊어버렸을 때 프로덕션 사고로 직결되는 작업은 결코 언어 모델의 자발적 도구 선택 확률에 맡겨두어서는 안 됩니다. 도구의 인터페이스는 가장 가벼운 CLI 스크립트로 다듬고, 그 실행의 방아쇠는 에이전트 바깥의 결정론적 라이프사이클 훅에 걸어두는 것만이 토큰 비용을 78% 이상 덜어내면서도 완벽한 엔지니어링 규율을 달성하는 길입니다.

← 목록으로