내 에이전트가 양식의 모든 필드를 채웠지만, 세 개의 이미지는 첨부하지 못했습니다.
요약
Claude Code 환경에서 AI 에이전트가 파일 업로드 도구를 사용할 때 발생하는 권한 및 경로 제한 문제를 다룹니다. 사용자가 명시적으로 공유한 디렉토리나 세션 내 폴더임에도 불구하고 파일 업로드가 거부되는 기술적 제약 사항을 분석합니다.
핵심 포인트
- Claude Code의 file_upload 도구는 엄격한 파일 공유 범위 제한을 가짐
- 세션 내에서 직접 생성한 파일이라도 특정 조건 미충족 시 업로드 불가
- 채팅에 첨부된 이미지는 디스크 파일로 변환되지 않아 도구 사용이 어려움
- 공유 가능한 경로는 채팅 첨부물, outputs/uploads 폴더, 연결된 폴더로 한정됨
저는 웹 서비스의 리스팅 양식을 작성하는 AI 에이전트를 사용하고 있었습니다. 모든 텍스트 필드가 채워졌고, '임시 저장(Save Draft)' 버튼도 클릭했습니다. 남은 것은 세 개의 PNG 파일을 첨부하는 것뿐이었습니다.
하지만 그 부분은 전혀 작동하지 않았습니다.
저는 양식을 채울 수 있다면 파일도 첨부할 수 있을 것이라고 가정했습니다. 하지만 그것은 두 개의 서로 다른 기능이었고, 서로 다른 지점에서 제한(gated)되어 있었으며, Claude Code를 어떻게 실행하느냐에 따라 그 제한 방식이 다르게 작동했습니다. 동일한 기기, 동일한 설정, 동일한 브라우저 확장 프로그램을 사용했음에도 결과는 달랐습니다.
제 설정은 다음과 같습니다: Windows 11 환경의 Claude Code 데스크톱 앱, Chrome의 Claude 확장 프로그램, 그리고 computer-use 데스크톱 자동화 서버입니다.
승인된 경로가 먼저 차단됨
확장 프로그램은 에이전트에게 file_upload라는 도구(tool)를 제공합니다. 이 도구의 설명에는 사용 방법과 주의 사항이 정확히 명시되어 있습니다:
페이지의 파일 입력(file input) 요소에 하나 또는 여러 개의 파일을 업로드합니다. 파일 업로드 버튼이나 파일 입력 요소를 클릭하지 마십시오. 클릭하면 볼 수 없거나 상호작용할 수 없는 네이티브 파일 피커(file picker) 대화 상자가 열립니다. 대신,
read_page또는find를 사용하여 파일 입력 요소를 찾은 다음, 이 도구의 ref를 사용하여 파일을 직접 업로드하십시오. 사용자가 이 세션과 공유한 파일(첨부 파일, 세션의 outputs/uploads 폴더, 또는 사용자가 연결한 폴더)만 업로드할 수 있으며, 다른 경로는 거부됩니다. 단일 호출 시 모든 파일의 합계 크기는 10MB 미만이어야 합니다.
처음 세 문장은 절차를 설명하며, 이 절차는 즉시 작동했습니다: 입력을 찾고, ref와 경로를 전달하는 방식입니다. 문제는 네 번째 문장에서 발생했습니다.
Cannot upload "...": only files the user has shared with this session can be uploaded.
저는 파일을 "공유"하기 위해 네 가지 방법을 시도했습니다. 세션이 시작된 순간부터 세션의 허용 목록에 있던 디렉토리, 세션 자체의 임시(temp) 폴더, directory-connect 도구를 통해 승인된 폴더, 그리고 settings.local.json의 additionalDirectories에 추가된 폴더입니다. 네 가지 방법 모두 동일한 오류 메시지가 반환되었습니다.
첫 번째 방법은 다시 살펴볼 가치가 있습니다. 해당 허용된 디렉토리는 세션이 읽고 쓰는 곳이며, 저는 에이전트가 그곳에 테스트용 PNG를 생성하도록 했습니다. 세션은 자신이 직접 작성한 파일이라 할지라도, 명시적으로 쓰기 권한이 허용된 디렉토리에 30초 전에 저장한 파일을 스스로 업로드할 수 없습니다.
채팅에 이미지를 첨부하는 것도 도움이 되지 않았습니다. 왜냐하면 이 표면상으로는 채팅 첨부물이 파일로 변환되지 않기 때문입니다. 모델은 이미지 데이터를 받지만, 디스크에는 아무것도 남지 않습니다. 임시(temp) 트리, 설정(config) 트리, 그리고 앱의 데이터 폴더를 모두 스캔해 보았지만 새로운 PNG는 하나도 없었습니다. 별도의 이미지 업로드 도구(image-upload tool)가 있긴 하지만, 이는 imageId를 필요로 하며 채팅 첨부물에는 해당 ID가 포함되지 않습니다.
이 중 어느 것도 버그가 아닙니다. 해당 설명의 괄호 안 내용은 "공유된(shared)"의 범위를 정확히 세 가지로 제한합니다: 채팅 첨부물, 세션의 출력/업로드(outputs/uploads) 폴더, 그리고 사용자가 연결한 폴더입니다.
좋습니다 — 그렇다면 파일을 그 세 가지 중 하나에 넣으면 됩니다. 하지만 세션 디렉토리 안에는 outputs나 uploads 폴더가 없습니다. 그래서 제가 직접 폴더를 만들고 각각에 PNG를 넣은 뒤 다시 시도해 보았습니다:
Cannot upload "...\outputs\test-out.png": only files the user has shared with this session can be uploaded.
Cannot upload "...\uploads\test-up.png": only files the user has shared with this session can be uploaded.
이름을 맞춰보아도 변하는 것은 없었습니다. 그리고 "사용자가 연결한 폴더"가 디렉토리 연결 도구(directory-connect tool)를 의미할 수는 없습니다. 왜냐하면 그 도구는 이미 거부 목록에 올라가 있기 때문입니다. 이 표면상으로는 나열된 세 가지 탈출구(escape hatches)가 모두 장식에 불과합니다.
네이티브 대화 상자에 접근할 수 없습니다
당연히 다음에 들 법한 생각은 이렇습니다: 버튼을 누르면 파일 선택기(file picker)가 열리니, 파일 선택기를 제어하면 된다는 것입니다.
하지만 그 창은 파일 탐색기(File Explorer)가 아닙니다. 그것은 chrome.exe가 소유한 공통 파일 대화 상자(common file dialog)입니다. 데스크톱 자동화(Desktop automation)는 최상단 창을 소유한 프로세스별로 권한을 결정하므로, 자동화 시스템 입장에서 그 대화 상자는 곧 Chrome입니다.
그리고 브라우저는 computer-use (컴퓨터 사용) 기능에서 "read" (읽기) 티어(tier)로 고정되어 있습니다. 스크린샷은 가능하지만, 클릭과 키스트로크 (keystrokes)는 불가능합니다. 액세스 권한을 요청하면 (granted tier:"read"가 반환됨), Chrome을 전면으로 가져온 뒤 클릭을 시도해 보세요:
"Chrome"은 "read" 티어로 권한이 부여되었습니다 — 스크린샷으로는 보이지만, 클릭이나 타이핑은 불가능합니다.
... 이 앱에 클릭이나 키스트로크를 보내기 위해 AppleScript, System Events, 셸 명령(shell commands) 또는 기타 어떤 방법을 사용해서도 안 됩니다.
앞부분은 현재 상태를 나타냅니다. 뒷부분은 당신이 막 생각하려던 우회 방법들을 나열한 뒤, "또는 기타 어떤 방법(or any other method)"이라는 문구로 목록을 종결하며 세 번째 경로마저 차단합니다. 즉, File Explorer (파일 탐색기)는 전체 티어(full tier)에서 제어가 가능하므로, 탐색기에서 브라우저의 드롭 존 (drop zone)으로 파일을 드래그하는 방식이 가능해 보이지만, 그것 역시 Chrome에 입력을 보내는 방식이라는 점을 깨닫기 직전까지만 유효할 뿐입니다.
추가적인 함정: 만약 버튼을 클릭한다면, 당신이 연 대화 상자는 이제 닫을 수 없는 상태가 됩니다. Chrome은 read-tier (읽기 티어)이므로 Escape 키조차 보낼 수 없습니다. 대화 상자는 사람이 직접 와서 닫아줄 때까지 모달 (modal) 상태로 그대로 남아 있게 됩니다.
같은 도구 이름, 다른 관문
세 가지 경로가 모두 막히자 저는 문서를 찾아보았고, 버전 요구 사항을 발견했습니다: 로컬 파일 업로드 기능은 Claude Code v2.1.211.1 버전부터 도입되었습니다. 여기에는 세 가지 제한 사항이 따릅니다 — 세션이 해당 파일을 읽을 수 있도록 허용되어야 하며, 호출당 총 10MB 이내여야 하고, 여러 개의 하드 링크 (hard links)가 있는 파일은 거부됩니다.
그래서 저는 해당 버전 이후의 버전에서 CLI (claude --chrome)를 통해 동일한 작업을 실행했습니다. 거부 메시지는 이전과 달랐습니다:
이 세션에서 읽을 수 있도록 허용된 파일만 가능합니다 ... /add-dir로 해당 폴더를 추가하세요
테스트의 초점이 "공유되었는가"에서 "읽을 수 있는가"로 옮겨갔습니다. 저는 Downloads 폴더에 /add-dir를 실행했고, file_upload가 즉시 통과되었습니다. 세 개의 PNG 파일, 총 731 KB, 페이지 JavaScript가 미리보기를 렌더링했으며, 세 개의 슬롯이 모두 채워졌습니다.
한편, 동일한 머신에서 데스크톱 앱(desktop app)은 여전히 거부합니다. 앱은 동일한 프로젝트에 머물러 있고, 방금 CLI가 사용했던 것과 동일한 설정 파일(settings file)을 읽지만, 기존의 "사용자가 이 세션과 공유한 파일만 가능합니다"라는 메시지를 반환합니다. 앱에 수정 사항을 입력하면 다음과 같은 결과가 나옵니다:
/add-dir isn't available in this environment.
두 경우 모두 확장 프로그램(extension)은 동일하므로(Chrome의 Claude 1.0.81), 차단 요소는 확장 프로그램에 있는 것이 아닙니다. 이는 각 인터페이스(surface)별로 구현된 Claude Code의 문제입니다.
이제 측정값이 아닌 문서(documentation)에 관한 부분입니다. 게시된 권한 규칙은 제 사례가 작동해야 하는 것처럼 읽힙니다:
권한: Claude는 세션이 파일을 읽을 수 있도록 허용된 경우에만 파일을 업로드할 수 있습니다.
읽고, 업로드하십시오. 하지만 제 측정 결과는 다릅니다. 이를 사양 위반(spec violation)이라고 부르기 전에 — 해당 페이지는 스스로를 CLI 또는 VS Code 확장 프로그램을 통한 브라우저 자동화(browser automation)로 한정(scoping)하여 설명하고 있습니다. 데스크톱 앱은 전혀 언급되지 않았습니다. 저는 제가 사용하는 인터페이스가 아닌, 바로 옆의 인터페이스에 대한 매뉴얼을 읽고 있었던 것입니다.
데스크톱 앱에는 자체 매뉴얼이 있습니다.2 심지어 /add-dir 결과에 대해서도 설명하고 있습니다. 즉, 대화형 터미널 패널을 여는 명령은 Code 탭에서 다르게 동작하며, 인자(argument) 형식이 없는 명령은 isn't available in this environment라고 응답한다는 것입니다. 제가 뚫어지게 쳐다보던 그 메시지는 처음부터 문서화된 동작이었습니다.
그 매뉴얼에는 브라우저 섹션도 있습니다. 하지만 그것은 Chrome 확장 프로그램이 아닌, 앱 자체에 내장된 브라우저인 브라우저 창(Browser pane)을 설명합니다. 브라우저 창에는 파일 첨부 기능이 전혀 없습니다. 해당 페이지에는 "업로드(upload)\
즉, 문서화된 조건이 위반된 것이 아닙니다. 문서화되지 않은 영역(undocumented surface)이었던 것입니다. 이를 확인하려면 두 매뉴얼을 모두 읽어야 했습니다.
여러분의 시간을 한 시간 정도 아껴드릴 각주가 하나 있습니다. 만약 데스크톱 앱(desktop app)을 사용 중인데 에이전트에게 현재 버전을 물어본다면, 에이전트는 claude --version을 셸(shell)로 실행하여 npm CLI 버전을 보고할 것입니다. 저는 여기에 속았습니다. 셸은 2.1.215라고 말했지만, 실제 앱은 AppData\Roaming\Claude\claude-code\ 내부에 포함된 2.1.219 버전을 실행 중이었습니다. 데스크톱 측면의 버전 명령어가 없기 때문에, 유일하게 신뢰할 수 있는 답변은 실행 중인 프로세스의 실행 파일 경로(executable path)뿐입니다.
클립보드 경로가 작동합니다
목표는 여전히 달성 가능했습니다. 단지 정문(front door)을 통하지 않았을 뿐입니다. OS 클립보드와 신뢰할 수 있는 붙여넣기 이벤트(paste event)를 이용하는 방법입니다:
- PowerShell에서
Set-Clipboard -LiteralPath <file>실행 (이렇게 하면 클립보드에 CF_HDROP가 담깁니다) - 페이지에
paste리스너(listener) 설치 - 브라우저 자동화(browser automation)를 통해 실제
Ctrl+V전송 clipboardData.files에서 파일을 가져와DataTransfer를 통해input[type=file].files에 넣고,change이벤트를 디스패치(dispatch)함
저는 실제 양식의 이미지 슬롯에 이 방법을 실행했습니다. 307 KB 크기의 PNG 파일이 이름, 크기, MIME 타입을 유지한 채 clipboardData.files에 도착했고, 페이지의 JavaScript가 이를 수락했으며, 미리보기가 렌더링(render)되었고, 저장까지 완료되었습니다. 파일이 어디에서 왔는지에 대한 어떠한 질문도 던져지지 않았습니다.
여기서 두 가지가 제 시간을 낭비하게 만들었습니다. 수정 키(modifier)를 별도의 파라미터로 지정하는 것은 아무런 도움이 되지 않습니다. 페이지에 문자 그대로 v가 입력될 뿐입니다. 키는 반드시 ctrl+v라는 문자열로 전달되어야 합니다. 그리고 좌표 클릭(coordinate clicks)은 먼저 스크린샷을 찍지 않으면 아무 곳에도 닿지 않습니다. 좌표 공간(coordinate space)이 스크린샷에 의해 정의되기 때문입니다.
작동하는 것과 권장할 만한 것은 서로 다른 문제이며, 이 경로는 네 가지 문제를 가지고 있습니다. 첫째, 이 방식은 file_upload에 걸려 있는 원점 제한(origin restriction)을 우회합니다. 이 제한은 아마도 에이전트가 아무도 모르게 로컬 파일을 웹으로 전송하는 것을 방지하기 위해 존재할 것이며, 이는 단순히 작업에 방해가 된다고 해서 비활성화할 수 있는 종류의 것이 아닙니다. 둘째, 클립보드(clipboard)를 덮어쓰기 때문에 사용자가 복사해 두었던 내용은 모두 사라지며, 에이전트가 이를 조용히 수행할 경우 오작동으로 간주됩니다. 셋째, 페이지가 어떻게 구축되었느냐에 따라 결과가 달라집니다. 페이지가 input.files를 감시하고 있다면 괜찮지만, 커스텀 업로더(custom uploader)가 drop 이벤트만 수신한다면 작동하지 않습니다. 넷째, 클립보드에 CF_HDROP를 넣는 방식은 Windows 전용입니다. 이 모든 것 외에도, 이는 지원되는 경로가 아니므로 업데이트를 통해 예고 없이 차단될 수 있습니다.
여기서 질문이 하나 생깁니다. 만약 데스크톱 앱에서 승인된 경로를 알고 있거나, 위의 네 가지 문제 없이 이를 수행할 방법을 알고 계신다면 알려주시기 바랍니다. 제가 아직 테스트해보지 않은 두 가지 후보는 앱의 내장 터미널에서 claude --chrome을 실행하는 것과 VS Code 확장을 사용하는 것입니다. 두 방법 모두 게이트의 CLI 측면에 도달할 것으로 보이지만, 아직 확인하지는 못했습니다.
반나절 동안 얻은 것
file_upload라는 하나의 도구 이름에 두 가지 동작이 존재합니다. CLI는 세션이 읽을 수 있는 모든 것을 수락하지만, 데스크톱 앱은 파일이 어디에서 왔는지 심문하고 거부합니다. 에이전트에게 넘겨줄 작업을 설계할 때, 저는 권한(permissions)을 설정 파일의 속성으로만 취급해 왔습니다. 하지만 권한은 실행 중인 바이너리(binary)가 무엇인지에 따른 속성이기도 합니다.
실질적으로, 첨부 파일이 포함된 양식 작업을 위해서는 세 가지 옵션이 남습니다. CLI에서 실행하거나, 데스크톱 앱을 위해 클립보드 절차를 준비해 두거나, 아니면 첨부 단계를 사람에게 맡기는 것입니다. 제 자신의 런북(runbook)에는 이미 신뢰할 수 있는 경로는 수동 작업이라고 적혀 있었습니다. 네 번째 경로를 구축하는 데 반나절을 쓰고 나서야 비로소 그 사실을 믿게 되었습니다. 알려진 2분짜리 수동 단계가 미지의 완전 자동화 단계보다 낫다는 사실은, 분명 수업료를 지불하고 얻은 교훈입니다.
확인하지 못한 것
- "사용자가 연결한 폴더(folders the user has connected)\
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기