이틀간 Claude Fable 5를 직접 써본 결론은 하나다. 집요할 만큼 능동적이라는 것. 온갖 기법을 두루 꿰고 있으며, 목표에 도달하기 위해 가능한 수단을 거의 모두 동원한다.
구체적인 예를 들어보겠다. 오늘 Datasette Agent를 만지다가 버그를 하나 발견했다. 점프 메뉴 채팅 프롬프트에 있어서는 안 될 가로 스크롤바가 나타나는 문제였다. 스크린샷을 찍어두었다.

그런 다음 datasette-agent 체크아웃 디렉터리에서 새 claude 세션을 열고, 스크린샷을 끌어다 놓은 뒤 이렇게 입력했다.
Look at dependencies to help figure out why there is a horizontal scrollbar here
원인이 Datasette Agent의 의존성(아마도 Datasette 자체)에 있을 것 같다는 직감이 있었다. Fable이 의존성 코드를 파고드는 데 능하다는 것도 알고 있었다. 자체 가상 환경 site-packages에 설치된 파일을 직접 뒤지거나, 디스크의 로컬 체크아웃을 참조하는 식으로 말이다. 의존성부터 살펴보라고 하는 게 좋은 출발점이 될 것 같았다.
그러다 집안일에 정신이 팔려 컴퓨터에서 잠시 멀어졌다.
몇 분 뒤 돌아와 보니, 내 컴퓨터가 평소 쓰는 Firefox에서 브라우저 창을 열더니 문제의 다이얼로그로 직접 이동하고 있었다. 브라우저 자동화를 쓰라고 지시한 적도 없고, Claude Code가 창 안에서 마우스 동작이나 키보드 단축키를 직접 실행할 수 있다고도 생각하지 않았는데, 도대체 어떻게 한 걸까?
흥미로운 마음으로 지켜보던 중, 이번에는 Firefox 대신 Safari 창이 열렸다. Claude 터미널에서 이런 화면도 포착했다.

uv run --with pyobjc-framework-Quartz로 무엇을 하고 있었던 걸까?
알고 보니 Fable이 브라우저 창 스크린샷을 찍는 방법을 스스로 고안해낸 것이었다. Python으로 내 컴퓨터의 열린 창을 전부 순회한 뒤, 창 이름에 "textarea" 같은 특정 문자열이 포함된 Safari 창만 골라냈다. 그렇게 해당 창의 번호—153551 같은 정수—를 알아낸 다음, screencapture CLI 도구로 PNG를 캡처하는 방식이었다.
스크린샷을 찍는 방법은 참 영리하다. 그런데 뭘 찍고 있었던 걸까?
버그를 재현하기 위해 직접 테스트용 HTML 페이지를 작성하고, Safari에서 열어 스크린샷을 찍고 있었다.
Fable이 만든 /tmp/textarea-scrollbar-test.html 페이지와, screencapture -x -o -l 153551 /tmp/safari-cases.png으로 찍은 스크린샷이 이것이다.
(탭이 너무 많이 열려 있다!)
테스트 페이지를 열고 스크린샷을 찍는 방법은 이해했다. 그런데 테스트 대상인 모달 다이얼로그는 어떻게 열었을까? 클릭이나 키보드 단축키로만 열 수 있는 것인데, Safari에서 그것을 실행할 수단이 보이지 않았다.
결국 그 방법을 알아냈다.
Claude는 애플리케이션 소스 코드가 담긴 폴더 안에서 실행되고 있었다. Datasette에 대해 충분히 알고 있는 터라 로컬 개발 서버를 직접 띄울 수 있었다. 알고 보니 Datasette의 템플릿을 직접 수정해, 창이 열리는 즉시 특정 키보드 단축키를 실행하는 JavaScript를 심어놓은 것이었다. 추가된 코드는 이런 식이었다.
<script>
window.addEventListener("load", function () {
setTimeout(function () {
document.dispatchEvent(new KeyboardEvent("keydown", {key: "/", bubbles: true}));
}, 1200);
});
</script>창이 열린 지 1.2초 후, 이 코드가 / 키 입력을 시뮬레이션한다. 모달 다이얼로그를 여는 키보드 단축키다.
아직 과제가 하나 남아 있었다. 정확한 원인을 파악하려면 페이지에서 JavaScript를 직접 실행해 수치를 측정해야 했다.
그러자 CORS를 통해 정보를 수집하는 웹 애플리케이션을 직접 만들어 로컬 서버로 띄우고, 해당 서버에 데이터를 POST하는 JavaScript가 담긴 페이지를 열었다!
표준 라이브러리 http.server 패키지로 작성한 Python 웹 앱은 다음과 같다.
from http.server import HTTPServer, BaseHTTPRequestHandler class H(BaseHTTPRequestHandler): def do_POST(self): n = int(self.headers.get("Content-Length", 0)) open("/tmp/diag.json", "w").write(self.rfile.read(n).decode()) self.send_response(200) self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() def do_OPTIONS(self): self.send_response(200) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Headers", "*") self.end_headers() def log_message(self, *a): # quiet pass HTTPServer(("127.0.0.1", 9999), H).serve_forever()
이 코드는 JSON이 담긴 POST 요청을 받아 /tmp/diag.json 파일에 기록하는 것이 전부다. 다른 도메인에서 실행되는 코드도 통신할 수 있도록 OPTIONS 요청을 포함한 Access-Control-Allow-Origin: * 헤더를 응답에 포함한다.
그런 다음 Claude는 브라우저에서 불러오는 템플릿에 이 코드를 주입했다.
const host = document.querySelector("navigation-search");
const ta = host.shadowRoot.querySelector("textarea");
const cs = getComputedStyle(ta);
fetch("http://127.0.0.1:9999/diag", {
method: "POST",
body: JSON.stringify({
dpr: window.devicePixelRatio,
scrollWidth: ta.scrollWidth, clientWidth: ta.clientWidth,
whiteSpace: cs.whiteSpace, width: cs.width,
}),
});이 코드는 <navigation-search> 웹 컴포넌트 내부의 <textarea> 수치를 측정해 서버로 전송하고, 서버는 이를 디스크의 파일에 기록한다. Claude는 그 파일을 읽어 정보를 얻었다.
이 모든 방법을 동원한 끝에 Fable은... 어딘가 보이지 않는 제한에 걸려 Opus로 다운그레이드되었다. 다행히 Opus는 전체 대화 기록에 접근할 수 있었고, Fable이 개척해 놓은 방법들을 그대로 이어받아 계속 진행할 수 있었다. 얼마 지나지 않아 수정 방법을 찾아내고, 테스트하고, 검증까지 완료했다.
Opus에게 다음과 같이 요청했다.
Write a report in /tmp/automation-report.md where you note down all of the tricks you have used in this session to test against real browsers on my computer, include runnable code examples
그 결과 이 리포트가 만들어졌다. 이 글을 쓰면서 전체 과정을 정리하는 데 큰 도움이 되었다.
Claude Code 세션의 전체 터미널 기록도 함께 공유한다.
스크린샷 한 장과 한 줄짜리 프롬프트만으로, Claude Fable 5 + Claude Code는 다음을 해냈다.
defaults write com.google.chrome.for.testing AppleShowScrollBars Always 스크롤바 표시 설정 활성화 (이후 다시 비활성화)textarea-scrollbar-test.html HTML 문서 직접 제작osascript -e 'tell application "System Events" to tell process "firefox" to id of window 1' 차단됨을 확인uv run --with pyobjc-framework-Quartz python 우회 방법 고안/ 키를 트리거하기 위해 사이트 템플릿에 JavaScript 추가그야말로 집요할 만큼 능동적이다!
한편으로는, 결국 두 줄짜리 CSS 수정으로 끝난 버그 하나를 해결하기 위해 Fable이 이토록 극단적인 방법까지 동원하는 모습이 경이롭기까지 했다.
하지만 다른 한편으로는, 이 경험이 묵직한 경고가 된다. 코딩 에이전트는 터미널에서 직접 명령을 입력하는 사람과 똑같은 일을 할 수 있다. 최신 프론티어 모델은 온갖 기법을 꿰고 있으며, 심지어 누구도 미처 문서화하지 못한 방법까지 스스로 찾아낸다.
만약 Fable이 악의적인 지시를 받았다면—코드나 이슈 스레드에 숨겨진 프롬프트 인젝션 공격이든, 내가 무심코 터미널에 붙여넣은 무언가든—데이터 유출이나 다른 형태의 피해를 일으키기 위해 얼마나 멀리 나아갈 수 있을지 생각하면 섬뜩하다.
샌드박스 밖에서 코딩 에이전트를 실행하는 것은 언제나 위험한 일이었다. Johann Rehberger가 AI의 일탈 정상화에서 설명한 챌린저 참사형 사고가 일어날 가능성이 가장 높은 시나리오로 꼽을 만하다.
Fable은 더 똑똑한 만큼 악의적인 지시에 더 의심을 품을 것이다. 하지만 그 똑똑함은 양날의 검이기도 하다. 만약 지시에 의해 실제로 조종당한다면, 집요한 능동성을 갖춘 이 모델이 얼마나 큰 피해를 입힐 수 있는지는 생각하기도 두렵다.
이 글은 블로그의 장문 아티클만 모아 보여줍니다. 모든 포스트를 받아보려면 /atom/everything/을 구독하거나, 다른 구독 옵션을 확인해 보세요.