보안 모델

aipager는 나를 대신해 Claude Code를 실행하고, Telegram에서 조종돼요. 그래서 두 가지 질문이 생겨요:

GitHub에서 원본 보기 ↗이 페이지는 영어 페이지를 번역한 거예요. 둘이 다르면 영어 페이지가 맞아요. 영어 원문
  1. 누가 봇을 조종할 수 있나요?
  2. 조종될 때 봇은 무엇을 할 수 있나요?

신뢰 경계#

봇이 여는 모든 처리기(메시지, 파일, 음성 메시지, 버튼)는 ~/.config/aipager/aipager.yaml에 설정된 채팅으로 만든 python-telegram-bot의 채팅 필터를 거치고, 명령은 aipager 자체 권한 부여 단계의 같은 확인을 거쳐요. 유일한 예외는 설정되지 않은 그룹에서 보낸 사람을 기록하기만 하는 처리기예요(아래 참고). 설정된 채팅만 봇과 상호 작용할 수 있어요. 다른 채팅에서 온 것은 처리되지도 않고 세션으로 넘어가지도 않아요. 설정되지 않은 개인 채팅은 답변 하나만 받아요: “이 봇은 아직 이 계정과 대화하도록 설정되어 있지 않아요”. 설정되지 않은 그룹에서 봇은 절대 답하지 않지만, 거기서 봇에게 메시지를 보낸 사람(그 사람의 Telegram ID, 사용자 이름, 이름, 그리고 그룹의 ID, 제목, 종류)을 ~/.claude/aipager-pending-users.json에 기록해서, aipager가 실행되는 동안 aipager config가 새 그룹과 그 안의 사람들을 찾을 수 있게 해요. 누구나 봇을 그룹에 추가할 수 있으니 누구나 이 파일에 줄을 더할 수 있어요. 그래도 그 사람에게 아무 권한도 생기지 않아요. 이 파일은 나만 읽을 수 있고(권한 0600), 쓸 때마다 통째로 바뀌고(절대 반만 쓰이지 않아요), 최근 200명까지 보관하며 30일 동안 나타나지 않은 사람은 지워요. 팀 모드에서는 그 위에 역할이 붙은 허용 목록이 하나 더 있고, 그룹에서는 봇이 그룹의 관리자이든 아니든 봇에게 보낸 메시지만 처리해요(명령, 봇 메시지에 대한 답장, 봇을 멘션한 메시지, 또는 봇 키보드를 누른 것)(그룹 참고).

그래서 바깥 세상에 열려 있는 것은:

  • 봇 token(비밀).
  • 채팅 ID(긴 정수).

둘 다 새어 나가면 공격자가 내 aipager를 조종할 수 있어요. token만 새어 나가면 공격자는 내 채팅에서 봇에게 보낸 메시지를 읽을 수 있지만, aipager가 따를 명령은 보낼 수 없어요. 채팅 ID만 새어 나가면 별일 없어요. 봇이 Telegram과 대화하려면 token이 필요하니까요.

비밀 정보#

비밀위치권한
봇 token~/.config/aipager/aipager.yaml기본 600
채팅 ID같은 파일기본 600

token은 aipager의 로그에 나오지 않아요. token이 들어 있는 HTTP 요청 줄은 기록하지 않고, 오류 메시지, aipager doctor, 설정 마법사는 봇 token처럼 보이는 것을 모두 가려요. 채팅 ID는 aipager의 로그에 나와요(예: 속도 제한에 관한 줄). 그러니 aipager logs의 출력은 개인적인 것으로 다루세요. 둘 다 저장소에 commit되지 않아요. 설정은 저장소가 아니라 사용자의 ~/.config에 있어요. Trusted Publisher를 통한 PyPI 배포 과정도 비밀 정보를 절대 건드리지 않아요. 인증은 OIDC가 맡아요.

token이 새어 나갔다고 의심되면 @BotFather에서 취소하고(/revoke), 새 token을 만들고, aipager config를 다시 실행하세요.

Claude 로그인 정보: 무엇이 정말로 지켜 주나요#

aipager는 내 UNIX 사용자로 Bash를 쓸 수 있는 자율 에이전트를 실행해요. 그래서 aipager가 읽을 수 있는 비밀은 aipager가 시작한 에이전트도 읽을 수 있어요. 잘못된 종류의 보호에 기대기 쉬우니까 분명히 말해 둘 만해요.

~/.config/aipager/는 safety.py의 금지 목록에 있고, 이 목록은 Telegram에서 조종되는 세션이 그 안의 파일을 읽지 못하게 막아요. 이것은 경로 기반 보호이고, 여기서는 소용없어요. Claude 로그인 정보(CLAUDE_CODE_OAUTH_TOKEN 또는 ANTHROPIC_API_KEY)는 경로로 얻는 게 아니라, 시작되는 세션마다 자기 프로세스 환경으로 넘겨지니까요. 세션 안에서 echo $CLAUDE_CODE_OAUTH_TOKEN은 금지 목록이 있든 없든 그것을 출력해요. 파일 경로 규칙은 환경 변수를 지키기에 맞지 않는 도구예요.

로그인 정보는 어디에 있나요#

배포 방식출처권한
systemd 사용자 서비스LoadCredential=claude_oauth:%h/.config/aipager/daemon.env, $CREDENTIALS_DIRECTORY/claude_oauth에서 읽어요600 (daemon.env). systemd는 로그인 정보를 서비스 자체 환경 밖에 둬요
macOS / Docker / systemd 없음~/.config/aipager/daemon.env, 직접 읽어요600

두 경로 모두 같은 코드(aipager/daemon_secrets.py)가 읽고, 자식 프로세스의 env= 표로 시작되는 세션에 넘겨요. bash -c 명령 문자열 안에 절대 넣지 않아요. /proc/PID/cmdline은 누구나 읽을 수 있고(0444), /proc/PID/environ은 0400(소유자만)이에요.

LoadCredential=과 0600이 정말로 해 주는 것은 이거예요. 같은 컴퓨터의 다른 UNIX 사용자가 로그인 정보를 읽을 수 없고, systemctl show, 서비스 파일 백업, 봇 소유자가 버그 신고에 붙여 넣는 journalctl 화면에 절대 나오지 않아요. aipager와 aipager가 시작한 에이전트 사이에는 아무 경계도 만들지 않아요. 그런 경계는 없고, 파일 권한을 아무리 조정해도 만들 수 없어요. 에이전트의 일 자체가 같은 환경 안에서 봇 소유자로 행동하는 것이니까요.

정말로 효과가 있는 유일한 방법#

로그인 정보 자체를 제한하세요. aipager에는 별도의 Anthropic 계정이나 지출 한도가 있는 API 키를 쓰고, 내가 claude에 대화형으로 로그인할 때 쓰는 token은 쓰지 마세요. aipager의 로그인 정보를 취소했을 때 사람이 로그아웃되는 일은 절대 없어야 해요. 그렇게 된다면 둘이 같은 로그인 정보이고 어느 쪽도 제한되지 않은 거예요. aipager doctor --fix는 daemon.env에 넣을 기존 token을 찾아 줄 수 있지만, 어떤 token을 줄지는 봇 소유자가 정하고, 그것이 이 문서에서 주변 파일 권한보다 중요한 유일한 결정이에요.

짐작 없이 로그인 문제 진단하기#

aipager doctor(그리고 aipager가 시작할 때마다 한 번 보내는 한 줄 알림)는 직접 만든 파일 확인이 아니라 Claude Code 자체의 claude auth status로 로그인 상태를 알려요. 세 가지 상태는 나오는 곳마다 항상 다르게 적혀요:

  • auth: <method> (<source>): 로그인되어 있음을 확인했어요.
  • auth: none (not logged in): 로그인되어 있지 않음을 확인했어요.
  • auth: unknown (...): 확인 자체가 실패했거나(시간 초과, 프로그램 없음, 읽을 수 없는 출력) 프로그램이 JSON 출력이 추가된 버전보다 오래됐어요. 이 경우는 절대 “로그인 안 됨”으로 알리지 않아요. aipager는 확신하지 못하는 로그인 확인 때문에 세션 시작을 거부하지 않아요. macOS 키체인으로 하는 로그인, refresh token만 있는 로그인 정보, 파일을 쓰지 않는 Max 요금제 로그인은 어떤 파일 확인으로도 보이지 않고, 그렇지 않았다면 “로그아웃됨”과 똑같아 보였을 거예요.

도구 호출을 실행할지 누가 정하나요#

도구 호출을 실행할지는 서로 다른 두 확인이 정하고, 이 순서로 실행돼요:

  1. aipager의 안전 검사. Claude Code는 도구를 호출하기 전마다 aipager의 PreToolUse hook을 실행해요. Telegram에서 시작한 작업이라면 hook이 그 호출을 보낸 사람 역할의 규칙과 안전 기준선(보호된 경로, 명령 패턴)에 따르게 하고, 허용되지 않는 것은 거부해요. 그러면 Claude Code는 자기 규칙이 무엇이라고 하든 아무에게도 묻지 않고 그 도구를 거부해요. 터미널에서 입력한 메시지는 이 확인의 제한을 받지 않아요. 아래 팀 모드의 규칙 적용에서 설명하는 내용이 여기에 들어가요.
  2. Claude Code 자체의 규칙(~/.claude/settings.json, 세션의 권한 모드). 물어보라고 하면 Claude Code가 PermissionRequest를 보내고, aipager가 Telegram에 권한 요청을 보여 줘요(hook 참고). 내가 누른 것은 hook의 결정으로 Claude Code에 돌아가거나, 20초 기다림이 지났다면 Claude Code 자체 대화 상자에 입력돼요. Claude Code의 규칙이 호출을 거부하면 Telegram까지 오지 않아요.

그래서 aipager는 Claude Code가 허용할 호출을 거부할 수 있고, Claude Code가 물을 때 aipager가 스스로 “예”라고 답하는 일은 절대 없어요. 허용은 항상 사람의 누름이에요. 묻지 않고 호출을 실행하게 하는 것은 세 가지예요:

  • 관리자를 위한(그리고 자기 개인 채팅 안의 사람을 위한) 전체 권한 모드. 전체 권한 모드의 세션은 --dangerously-skip-permissions로 시작해요. Claude Code가 아무것도 묻지 않으니, aipager의 안전 검사가 거부하지 않는 호출은 모두 누르지 않아도 실행돼요. 전체 권한 모드로 세션을 시작하거나, 세션을 이 모드로 바꾸거나(/mode full, 확인을 거쳐서), 이 모드로 세션을 재개할 수 있는 사람은 관리자(owner와 admin 역할, 또는 can_manage가 있는 역할)뿐이에요. 예외는 하나예요. 봇과의 자기 개인 채팅에서, 역할이 메시지를 보내고 Claude에게 답할 수 있는 사람(can_prompt와 can_approve, 즉 user이고 read_only는 아님)은 자기 세션에 같은 일을 할 수 있어요(/new !x1, 또는 그 채팅에서 “새 세션”을 전체 권한으로 설정, /mode full, /resume). Telegram과 Mini App 모두에서요. 그룹이나 다른 사람의 개인 채팅에서는 절대 안 돼요. 그 밖의 사람은 대신 자동 모드를 받고, 답변이 그렇다고 알려 줘요. 전체 권한 모드는 Claude Code 자체의 질문만 건너뛰어요. aipager의 PreToolUse hook은 확인 모드와 자동 모드에서와 똑같이 모든 호출에 그 사람 역할의 규칙(안전 기준선, 폴더 제한, 금지 규칙, 로그인 정보가 든 파일)을 적용하고, Claude Code가 알려 주는 모드가 이것을 느슨하게 만드는 일은 절대 없어요. 전체 권한 모드에서도 aipager의 안전 검사는 모든 호출에서 실행되지만, 소유자 자신의 작업과 터미널에서 입력한 메시지에는 아무것도 거부하지 않아요.
  • 기본값인 자동 모드. 자동 모드의 세션은 --permission-mode auto로 시작해요. Claude Code 자체의 안전 검사가 호출마다 살펴서 안전한 것은 실행하고 위험한 것은 멈추고, 멈춘 것은 확인 모드에서처럼 Telegram에 권한 요청으로 와요. 채팅이 /settings → 새 세션에서 다른 것을 고르지 않았다면 모든 세션은 자동 모드로 시작해요. 세션을 다룰 수 있는 사람은 누구나 확인 모드와 자동 모드 사이를 오갈 수 있어요(/mode). aipager는 세션에서 Shift+Tab을 눌러 그 자리에서 바꾸고, 전체 권한 모드를 Claude Code의 Shift+Tab 순환에 넣을 수 있는 옵션은 절대 쓰지 않아요. aipager가 전체 권한 모드로 시작하지 않은 세션에 대해 Claude Code가 전체 권한 모드라고 알려 오면, aipager는 알려진 대로 보여 주고, 로그에 경고를 남기고, 그것 때문에 세션을 전체 권한 모드로 시작하거나 재시작하는 일은 절대 없어요. 알려진 한계: Claude Code가 알려 주는 모드는 hook의 소켓으로 들어오는데, 이 컴퓨터의 어떤 프로세스든 그 소켓에 쓸 수 있어요(네트워크 연결 참고). 가짜 보고는 세션이 보여 주는 모드를 바꿀 수 있고, 확인 모드와 자동 모드 사이의 전환이 확인을 기다리는 동안이라면 aipager가 Shift+Tab을 한 번 더 누르게 해서, 요청한 것은 확인 모드인데 실행 중인 세션이 자동 모드에 머물게 할 수 있어요(다음 진짜 hook이 실제 모드를 다시 보여 줘요). 재시작이나 재개 때 넘기는 옵션은 절대 바꾸지 못하고, 전체 권한 모드에는 절대 닿지 못해요. 그 모드는 시작할 때의 옵션만 설정할 수 있으니까요. aipager는 전체 권한 모드로 순환할 수 있다고 아는 세션에서는 Shift+Tab을 누르지 않아요. 전체 권한 모드로 시작한 세션(나중에 터미널에서 확인 모드나 자동 모드로 바꿨더라도), 또는 실행 중인 aipager가 시작된 뒤 전체 권한 모드를 알려 온 세션이에요. 그런 세션에서는 모드를 바꿀 때마다 재시작해서 새 모드의 옵션을 넘기고 전체 권한 옵션은 빼요. 가짜 전체 권한 보고는 기껏해야 전환 하나를 재시작으로 바꿀 뿐이에요. 전체 권한 모드를 Shift+Tab 순환에 넣는 옵션(전체 권한 옵션 앞의 --allow-)으로 터미널에서 시작한 세션, 또는 --dangerously-skip-permissions로 시작했지만 aipager가 그 세션의 소식을 듣기 전에 전체 권한 모드를 떠난 세션(그때 aipager가 실행 중이 아니었거나, 그 뒤로 재시작한 경우)은 aipager가 알 수 없어요. 그런 세션에서는 확인 모드에서 자동 모드로 바꾸다가 전체 권한 모드에서 멈출 수 있고, 그러면 카드에 세션이 전체 권한 모드라고 나와요. 관리자가 아닌 사람이 Telegram에서 모드를 바꿀 수 있는 터미널 세션에는 그런 옵션을 쓰지 마세요. Telegram에서 전체 권한 모드를 쓸 수 있게 하려고 aipager는 시작할 때마다 ~/.claude/settings.json에 skipDangerousModePermissionPrompt를 설정해요(그렇지 않으면 이 모드로 실행하기 전 Claude Code의 경고가 세션을 멈춰 세워요). 이 설정은 내가 직접 시작하는 세션의 경고도 꺼요. 같은 이유로 ~/.claude.json에서 세션이 시작되는 폴더를 신뢰하는 폴더로 표시해요(그 폴더나 그 위의 폴더가 이미 신뢰되어 있으면 하지 않아요). 그래야 Claude Code의 “Do you trust this folder?” 질문이 Telegram에서 시작한 세션을 막지 않아요. 그렇지 않으면 첫 메시지가 “No, exit”으로 답해 버려요. 신뢰하는 폴더의 .claude/ 설정, .git/ hook, .mcp.json은 명령을 실행할 수 있어서, can_manage가 없는 멤버는 이 파일들을 절대 쓸 수 없어요(policy.yaml이 그 역할에 shell을 다시 주지 않는 한). Claude Code는 폴더의 신뢰를 그 아래에서 git 저장소 안에 있지 않은 폴더에도 적용해요. 저장소는 여전히 따로 질문을 받아요. 홈 폴더는 세션이 거기서 시작될 때 표시해요. Claude Code는 홈 폴더에서 한 '예'를 그 세션에만 기억하기 때문이에요. 그 뒤로는 홈 폴더 아래에서 git 저장소 안에 있지 않은 폴더라면 Claude Code가 아무것도 묻지 않아요. 내가 직접 시작하는 세션에서도요. /와 홈 폴더 위의 폴더는 절대 표시하지 않아요. 그러면 이 컴퓨터의 그런 폴더 전부에 적용되니까요. 그래서 그런 폴더에서 시작한 세션은 질문에서 멈춰요. aipager는 시작할 때 아무것도 표시하지 않고, 세션이 막 시작될 폴더만 표시해요. 새 세션은 aipager가 실행되는 폴더(Linux와 macOS의 aipager service install, 그리고 컨테이너에서는 홈 폴더)에서 시작하고, AIPAGER_WORK_DIR가 설정되어 있으면 그 폴더에서 시작해요. 그 폴더가 /나 홈 폴더 위의 폴더이고 신뢰되어 있지 않으면 aipager 로그가 시작할 때 경고해요. 이전 버전은 시작할 때마다 aipager의 폴더를 표시했고, macOS 서비스(/에서 실행됐어요)에서는 /도 표시했어요. aipager는 그런 항목과 내가 만든 항목을 구분할 수 없어서 그대로 둬요. aipager는 ~/.claude.json과 ~/.claude/settings.json을 전부 읽을 수 없거나, aipager가 업데이트하는 사이 실행 중인 Claude가 바꿨다면 건드리지 않아요.
  • 항상 허용은 Claude Code가 요청과 함께 준 “다시 묻지 않기” 규칙을 Claude Code에 돌려줘요. Claude Code 자체 대화 상자의 그 선택지와 똑같아요.

어떤 도구를 모두에게 막으려면 ~/.claude/settings.json에서 금지하세요. 일부 사람에게만 막으려면 policy.yaml의 그 사람 역할 규칙을 쓰세요(그룹 참고).

권한 요청에 보이는 것이 실제로 실행돼요. 권한 요청은 Claude의 설명 아래에 실제 명령이나 경로를 보여 줘요. Unicode 방향 제어 문자(U+202A-U+202E, U+2066-U+2069, U+200E, U+200F, U+061C)는 Telegram, 브라우저, 휴대폰이 그 뒤의 글을 그리는 순서를 바꾸지만, shell은 문자를 원래 순서대로 실행하니까, 어떤 명령을 다른 명령처럼 보이게 할 수 있어요(“Trojan Source” 수법, CVE-2021-42574). 모든 권한 요청, 질문, 변경 미리 보기, 감사 기록에서 이 문자들은 보이는 표시(⟨U+202E⟩)로 나오고, 권한 요청은 명령이 보이는 대로 실행되지 않을 수 있다고 경고해요. 다른 숨은 문자는 그대로 둬요.

팀 모드의 규칙 적용#

팀 모드에서 aipager는 누름 아래에 층을 하나 더 둬요. Telegram에서 오는 모든 메시지에는 권한 메모(누가 보냈고 그 역할이 무엇을 허용하는지)가 붙고, /tmp/claude-notes-<session>/에 메시지마다 파일 하나로 쓰여요. Claude가 메시지를 받으면 보조 프로그램 aipager-hook이 그것을 이 메모들과 맞춰 보고, 보낸 사람의 규칙을 PreToolUse 검사가 읽는 곳에 둬요. 나머지는 모두 다음 성질에 기대요:

  • 가장 엄격한 규칙이 이겨요. Claude가 한 작업에서 여러 사람의 메시지를 받으면(aipager는 이것을 피하려고 메시지를 보류하지만, 그래도 일어나도 안전해요), 그 작업은 가장 엄격한 조합으로 실행돼요. 소유자의 메시지가 다른 사람에게 자기 권한을 빌려주는 일은 절대 없어요.
  • 실행 중인 작업은 절대 넓어지지 않아요. 다른 사람의 메시지는 실행 중인 작업이 끝나기를 기다렸다가, 따로 작업이 되어 보낸 사람의 규칙으로 실행돼요. 그래도 메시지가 실행 중인 작업에 합쳐지면 그 작업은 그 안의 모든 사람 가운데 가장 엄격한 규칙을 유지해요. 내가 터미널에서 입력한 작업에 Telegram 메시지가 합쳐지면 그 작업의 남은 부분은 보낸 사람의 규칙을 따라요(소유자의 메시지는 아무것도 바꾸지 않아요).
  • 내 역할은 지금 다루고 있는 채팅에서의 역할이에요. 여러 채팅에 있는 사람(예를 들어 그룹에서는 user, 자기 개인 채팅에서는 owner)은 aipager.yaml의 채팅 순서와 상관없이, 그룹에서 보내거나 누르는 모든 것에는 그룹의 역할을, 개인 채팅에서는 개인 채팅의 역할을 가져요.
  • 모르면 제한해요. 누가 보냈는지 모르는 메시지는 기본 최소 권한(건너뛰기 권한 없음, 모든 금지 규칙 적용)으로 실행되고, 제한 없는 터미널 메시지처럼 실행되는 일은 절대 없어요. 내가 터미널에 직접 입력한 메시지는 여전히 제한되지 않아요. 어느 줄에든 Telegram 표시가 있는 것은 모두 확인해요. 모든 Telegram 메시지에는 이 표시가 있고, 그 메시지 자체를 보낸 사람의 이름이 적혀 있어요(세션을 마지막으로 다룬 사람이 아니에요). aipager가 모르는 보낸 사람, 예를 들어 메시지가 기다리는 동안 aipager.yaml에서 지워진 사람은 이름 없는 [via Telegram] 표시와 최소 권한을 받아요.
  • 슬래시 명령에는 관리자가 필요해요. 슬래시 명령은 아무 표시 없이 그대로 Claude에 입력되고, /deliver 같은 명령이나 skill은 자기 내용으로 작업 하나를 통째로 실행해요. 그래서 역할이 역할 규칙을 건너뛸 수 없는 멤버(기본 user와 read_only, 내가 만든 역할 가운데 bypass_role_denies가 없는 것, aipager가 모르는 사람)는 aipager의 명령어 키보드에 있는 명령(그리고 /compact)과 모델 변경(/model <name>)만 보낼 수 있어요. 그 밖의 슬래시 명령은 입력하든, /<name>으로 세션에 보내든, 파일 캡션으로 붙이든, /new의 첫 메시지로 쓰든, 템플릿으로 쓰든 모두 “이 명령은 관리자만 쓸 수 있어요.”와 🤷로 거부돼요. 소유자와 관리자는 모든 명령을 보낼 수 있어요. Telegram에서 Claude까지 간 슬래시 명령도 여전히 확인해요. hook은 그 작업이 이 명령을 보낸 Telegram 메시지를 받았다는 것을 보고 보낸 사람의 규칙을 따르게 해요. 내가 터미널에서 입력한 슬래시 명령은 제한되지 않아요. 다만 같은 명령(인수와 상관없이)이 이 세션에서 전에 Telegram으로 보내진 적이 있다면 예외예요. aipager는 내 명령과 Claude Code가 다시 보내는 Telegram의 명령을 구별할 수 없어서, 그때 내 명령은 가장 최근 Telegram 작업의 규칙으로 실행돼요. 이것은 세션이 끝나면 끝나요.
  • 새 보낸 사람이 없는 메시지는 작업의 규칙을 유지해요. Telegram에서 시작한 작업이 실행되는 동안 Claude Code는 어떤 Telegram 메시지로도 설명되지 않는 메시지도 알려 와요. 터미널에서 입력해서 그 작업 뒤에 줄 선 메시지(줄 서는 순간 알려 와요), 이 컴퓨터의 다른 Claude 세션에서 온 메시지, 또는 대화 압축 뒤 다시 보내진 같은 Telegram 메시지예요. 이런 메시지는 Telegram 보낸 사람을 더하지 않으니, 작업은 쓰던 규칙을 유지하고, 아직 받기를 기다리는 메시지 때문에 더 엄격해질 수는 있어도 절대 느슨해지지 않아요. Telegram 글이 있지만 누가 보냈는지 aipager가 모르는 메시지는 여전히 기본 최소 권한으로 떨어지고, 권한이 더 적은 사람의 메시지는 여전히 작업을 그 사람의 규칙으로 낮춰요.

제한된 사용자를 정말로 막는 것. Claude 세션은 aipager 설정(~/.config/aipager/aipager.yaml의 봇 token, ~/.config/aipager/daemon.env의 Claude 로그인 정보)을 소유한 것과 같은 운영 체제 사용자로 실행되고, 작업마다의 규칙 파일(/tmp/claude-policy-<session>.json)도 그 사용자 소유예요. 그 사용자로 실행되는 shell은 어떤 패턴도 예상하지 못한 방식(sed -i … /tmp/*-policy-*.json, glob을 도는 for 반복, Python 한 줄)으로 둘 다에 닿아서 자기 규칙을 소유자의 규칙으로 바꿀 수 있어요. 그래서 명령 패턴은 거름망이지 경계가 아니에요. 기본 제한 역할(user, read_only)로 실행되는 작업과, aipager가 누구의 것인지 모르는 작업에 aipager는 다음 규칙을 적용하고, 어느 것도 명령 패턴에 기대지 않아요(이 규칙이 무엇을 다루지 않는지는 아래 알려진 한계를 보세요):

  • shell이 없어요. Bash도, Claude Code가 코드를 실행하는 다른 도구도 쓸 수 없어요(PowerShell, Monitor, REPL, Workflow, 메시지 예약 도구 CronCreate와 RemoteTrigger 등. 목록은 safety.CODE_EXECUTION_TOOLS예요). 예약된 메시지는 Telegram 표시 없이, 즉 제한 없이 도착하니까 이 도구들도 들어가요. 같은 이유로 SendMessage(메시지를 이 컴퓨터의 다른 Claude 세션으로 보낼 수 있고, 거기서는 터미널 메시지처럼 실행돼요)도, EnterWorktree/ExitWorktree(세션의 작업 폴더를 바꾸는데, 다음 규칙이 그것에 기대요)도 쓸 수 없어요.
  • 파일 쓰기는 프로젝트 안에서만. Write, Edit, MultiEdit, NotebookEdit은 세션의 폴더(hook 데이터에서 Claude Code가 알려 주는 작업 폴더이고, 도구 호출의 경로가 절대 아니에요)와 세션의 Claude Code scratchpad(/tmp/claude-<uid>/<project>/<session id>/, 알 수 없을 때는 /tmp/claude-<uid>/) 안에만 써요. 다른 곳은 모두 거부돼요. 내 shell 시작 파일, ~/.ssh, 다른 프로젝트, /tmp에 있는 aipager 파일이에요. 폴더 안에서도 .claude/, .git/, .mcp.json은 거부돼요. 하나하나가 Claude Code나 git이 명령을 실행하게 하니까요. 결정하기 전에 심볼릭 링크를 따라가요. 폴더가 내 홈 폴더, /, 또는 홈 폴더 위의 폴더인 세션에는 프로젝트 폴더가 없어요. 거기서 제한된 작업은 자기 scratchpad에만 쓸 수 있고, 다른 곳에 쓰면 “이 세션은 홈 폴더(또는 그 위 폴더)에서 실행 중이고, 이 역할은 프로젝트 폴더 안에만 쓸 수 있어요”로 거부돼요. 제한된 멤버가 거기 갇히지 않도록 /new와 Mini App은 그런 폴더에서 세션을 시작하지 못하게 해요(새 세션의 기본값인 aipager 자체 폴더는 백그라운드 서비스로 실행할 때 내 홈 폴더예요). 대신 내 홈 폴더 안에 새 폴더를 만들어 거기서 시작할 수 있어요(새 폴더여야 해요. 내가 거기 이미 가진 폴더는 안 되고, 다른 프로그램이 코드를 불러오는 bin이나 node_modules도 안 돼요).
  • 멤버는 자기 루트 폴더 안에 있어요. can_manage가 없는 역할(기본 user와 read_only)은 채팅의 루트 폴더 안에서 일해요. 루트 폴더는 aipager config에서 내가 정하고(기본값 ~/aipager-users/<label>. ~/aipager-users와 그 폴더는 권한 0700으로 만들어요), /new, 재개, 새 세션으로 교체, 재시작, 모드 전환, Mini App은 그런 멤버의 세션을 그 폴더 안에서만 시작하거나 재시작해요. 메시지가 어느 길로 오든(Telegram, Mini App, 다른 세션의 메시지) 안전 hook이 이것을 지켜요. 루트 폴더는 작업의 규칙 스냅숏, 세션 사이 메시지의 영수증, 고정되거나 예약된 권한과 함께 다니고, 루트 폴더가 다른 멤버들이 한 작업을 함께 쓰면 더 좁은 쪽이 이겨요(서로 포함하지 않는 두 폴더라면 세션 자신의 폴더만 남아요). 세션의 폴더에 쓰는 것은 그 폴더가 루트 폴더 안에 있을 때뿐이에요(루트 폴더 밖의 세션, 예를 들어 루트 폴더가 생기기 전에 시작한 세션은 계속 실행되지만, 그 작업은 scratchpad에만 써요: “이 세션은 이 역할의 폴더 밖에서 실행 중이라…”). 그리고 Read, LSP, Grep, Glob은 내 홈 폴더 안에서 루트 폴더 밖의 것에 닿지 않아요(“홈 폴더에서는 이 역할이 자기 폴더 안만 읽을 수 있어요”). 세션의 scratchpad와 자기 답장 파일은 예외예요. 다른 모든 채팅의 루트 폴더는 어디에 있든 그 멤버에게 보호된 경로이고, /tmp/aipager-files/ 아래에 있는 다른 모든 채팅의 업로드 폴더도 마찬가지예요(Read, Grep, Glob과 쓰기 도구. Bash가 있는 역할이라면 그런 폴더를 적은 명령을 거부하지만, 최선의 노력일 뿐이에요). 채팅마다 폴더가 생기기 전에 모든 채팅이 거기 함께 올려 둔 파일도 포함돼요. 멤버 자신의 채팅에서 올린 파일은 여전히 읽을 수 있어요. 내 홈 폴더 밖의 폴더(/usr, /etc, /srv)는 여전히 읽을 수 있어요. bypass_role_denies나 bypass_safety는 있지만 can_manage가 없는 직접 만든 역할은 자기 루트 폴더 안 어디에든 쓰고 검색할 수 있고, 내 홈 폴더에서 읽는 것도 그 안에서만이에요. 제한이 없는 것은 소유자와 관리자뿐이에요. bypass_safety가 있으면, 폴더가 자기 루트 폴더 밖인 세션에서는 user처럼 scratchpad에서만 쓰고 검색할 수 있고, 도구 호출 하나가 거부되면 작업의 나머지가 멈춰요.
  • 검색은 프로젝트 안에서만. Grep이나 Glob은 다음이 모두 맞을 때만 허용돼요. 검색하는 폴더(path, 없으면 세션의 폴더, 심볼릭 링크를 따라간 뒤)가 세션의 폴더 안(쓰기와 마찬가지로 그것이 내 홈 폴더, /, 또는 홈 폴더 위의 폴더라면 세지 않아요)이나 scratchpad 안에 있어요. 그 폴더가 보호된 경로가 아니고 보호된 경로를 품고 있지도 않아요(내 홈 폴더에서 시작한 세션은 ~/.config/aipager를 품고 있으니 그 뿌리에서는 검색할 수 없어요). 모든 glob이 단순한 상대 패턴이에요(앞에 /, ~, $, 드라이브 문자가 없고, ..이 없고, #(ripgrep에서는 주석)이나 !(부정)이 없고, 이스케이프 문자 \가 없어요). 그리고 /나 ~로 시작하지 않는 보호된 경로 규칙이 없어요(**/.env는 프로젝트 어디에나 있을 수 있으니, 그런 규칙이 있으면 제한된 검색은 모두 거부돼요). 그 밖에는 모두 거부돼요. 일부러 허용 목록으로 만들었어요. 세 번의 검토에서 매번 ripgrep을 분석 모델이 예측한 폴더 밖으로 데리고 나가는 새 glob 작성법이 나왔고, 그 가운데 하나는 봇 token을 읽었어요.
  • 파일 도구는 보호된 경로를 존중해요. Read, LSP, 쓰기 도구는 경로가 실제로 어디로 이어지는지 심볼릭 링크까지 따라가서 확인해요. 작업이 읽을 수 있는 유일한 제어 파일은 그 세션 자신의 /tmp/claude-reply-<session>.txt이고, 내가 예전 메시지에 답장할 때 aipager가 메시지에 적어 주는 파일이에요. 다른 세션의 답장 파일, 그리고 그 파일에 쓰는 것은 여전히 거부돼요. macOS에서는 /tmp가 /private/tmp이고 디스크가 대소문자를 구별하지 않아서, 두 표기와 모든 대소문자 조합을 알아봐요.
  • 고장 난 확인은 거부해요. 도구 호출에 대해 결정하다가 오류가 나거나(경로 안의 NUL 바이트, 버그), 결정하는 동안 hook의 메모리가 바닥나면 그 호출은 거부돼요. 세션의 규칙을 읽을 수 있고 소유자에게 건너뛰기 권한을 준다면 예외예요. 예전에는 통과시켰어요.

내가 정하는 보호된 경로. policy.yaml의 safety: 부분(deny_paths_no_access, deny_paths_no_write, deny_bash_patterns)은 기본 보호 경로와 명령 패턴에 더해져요. 이것은 관리자를 포함해 owner를 뺀 모든 역할의 모든 작업이 받는 안전 기준선의 일부이고, 기본 목록과 똑같이 파일 도구, 검색, Bash 명령에 적용돼요. aipager는 합친 목록을 /tmp/claude-policy-.floor-<uid>.json(이것 자체도 보호된 경로예요)에 써서 안전 hook의 대체 경로가 쓰게 해요. 이 파일이 없거나, 읽을 수 없거나, 다른 운영 체제 사용자의 것이면 hook은 기본 목록을 쓰고, 그보다 적게 쓰는 일은 절대 없어요. 재시작 없는 다시 불러오기로 한 변경은 다시 불러온 뒤에 보낸 메시지에 적용되고, 실행 중인 작업이나 기다리는 메시지에는 적용되지 않아요. 0.7.20까지는 이 부분을 aipager doctor --safety-check가 읽어서 보여 주기만 하고 적용하지는 않았어요.

aipager가 누구의 것인지 모르는 작업. 따를 보낸 사람이 없는 Telegram 작업은 최소 권한으로 실행돼요. 기본 user 역할(내가 policy.yaml에서 user에 더한 제한은 빼고)에 내 safety: 부분을 더한 것과 같아요. 코드 실행 도구 없음, 제한된 쓰기와 검색, 보호된 경로예요. 권한 메모가 만료됐거나(24시간 뒤) 쓰이지 못한 메시지, 그리고 Telegram 글이 있지만 맞는 메모가 없는 메시지에 이런 일이 생겨요. 다시 시도 버튼은 다시 시도하는 메시지를 누른 사람이 직접 보냈다면 누른 사람의 권한으로 실행돼요. 그래서 소유자가 자기 메시지를 다시 시도하면 Bash를 유지해요. 다른 사람의 메시지나 aipager가 보낸 사람을 모르는 메시지를 다시 시도하면 최소 권한으로 실행돼서, 누구의 글도 누른 사람의 권한을 빌릴 수 없어요.

루트 폴더에 묶이지 않는 역할(can_manage는 있지만 건너뛰기 권한이 없는 직접 만든 역할)은 보호된 경로 밖을 읽을 수 있어요. 그 운영 체제 사용자의 다른 프로젝트를 읽을 수 있어요. 기본 user와 read_only는 내 홈 폴더에서 자기 루트 폴더 밖의 어떤 것도 읽을 수 없어요(위 참고). 로그인 정보가 든 내 파일은 bypass_role_denies가 없는 모든 역할(기본 user와 read_only, 내가 만든 역할, 최소 권한)에게 보호된 경로예요. ~/.ssh, ~/.gnupg, ~/.aws, ~/.config/gh, ~/.git-credentials, ~/.netrc, ~/.docker/config.json, ~/.kube, ~/.config/gcloud, ~/.azure, ~/.password-store, ~/.pgpass, ~/.npmrc, ~/.pypirc는 읽거나, 쓰거나, 검색할 수 없어요. 이것은 역할의 기본값이라서, policy.yaml에서 역할 자체의 deny_paths_no_access가 그 역할에 대해 이것을 대신해요. 다만 실행 중인 작업에 다른 메시지나 터미널에서 입력한 메시지가 합쳐지면, 그때부터 작업이 끝날 때까지 다시 이 목록에 묶여요(가장 엄격한 규칙이 이겨요). 다른 곳에 있는 로그인 정보 파일(프로젝트 안의 .env, 다른 폴더의 token)은 들어가지 않아요. 역할의 deny_paths_no_access에 추가하세요. 소유자는 이것에 묶이지 않아요. 관리자의 쓰기는 제한되지 않아요. 관리자의 Grep/Glob은 보호된 경로에 닿지 않는 어떤 폴더든 검색할 수 있어요. 검색하는 폴더(path, 없으면 세션의 폴더. Glob이라면 /home/you/.config/aipager/*처럼 패턴이 적은 폴더나, 와일드카드가 없는 패턴 위의 폴더도 들어가요. Claude Code는 그 이름을 어느 깊이에서든 찾으니까요)가 심볼릭 링크를 따라간 뒤 보호된 폴더이거나, 그런 폴더 안에 있거나, 그런 폴더를 품고 있으면 검색은 거부돼요. 그래서 ~, /, 내 홈 폴더 위의 폴더, /tmp에 대한 Grep은 거부되고(이 폴더들은 ~/.config/aipager, ~/.claude, aipager의 /tmp/claude-* 파일을 품고 있어요), 프로젝트나 /tmp/myproj에 대한 Grep은 거부되지 않아요. /나 ~로 시작하지 않는 보호된 경로 규칙(예: **/.env)은 어떤 폴더에서든 맞을 수 있어서, 그것이 관리자의 규칙에 있으면(또는 owner를 뺀 모든 역할이 받는 safety: 부분에 있으면) 관리자의 검색도 제한된 역할처럼 모두 거부돼요. aipager doctor는 safety: 부분의 그런 규칙에 대해 경고해요. 관리자에게는 Bash가 있으니 그 규칙은 여전히 최선의 노력일 뿐이에요(아래 참고).

알려진 한계. 다음은 다루지 않아요:

  • hook 자체가 결정을 시작하기도 전에 실패하면(예를 들어 자기 상태 파일을 읽을 수 없거나, 시작하는 동안 메모리가 바닥나면) Claude Code는 그것을 “허용”으로 받아들여요. 제한된 사용자는 이런 일을 일으킬 수 없어요. 결정하는 동안의 오류는 모두 거부로 이어져요.
  • 파일 도구는 아니지만 데이터를 밖으로 보낼 수 있는 도구는 경로를 확인하지 않아요. WebFetch(작업이 읽을 수 있는 것은 무엇이든 URL로 보낼 수 있어요)와 Claude Code의 다른 전달 도구예요. 이것이 중요하다면 policy.yaml에서 역할에 대해 금지하세요. 채팅으로 파일을 보내는 SendUserFile은 확인해요(Claude가 보내는 파일 참고).
  • 제한된 작업이 시작한 백그라운드 에이전트는 작업이 끝난 뒤에도 계속 실행되고, 이후의 도구 호출은 세션의 가장 최근 메시지를 기준으로 판단돼요. 나중에 소유자나 터미널에서 온 메시지가 그 제한을 풀어요.
  • 내가 정의한 역할 가운데 can_manage는 있지만 건너뛰기 권한이 하나도 없는 역할은 루트 폴더에 묶이지 않아요. 자기 채팅이 이미 일한 적 있는 어떤 프로젝트 폴더(이전 세션이 쓴 폴더)에서든 시작할 수 있고, 내 폴더도 포함되며, 내 다른 프로젝트를 읽을 수 있어요.
  • 어떤 멤버에게도 묶을 수 없는 작업(모르는 보낸 사람, 지워진 멤버, 보낸 사람을 확인할 수 없는 다른 세션의 메시지)은 제한된 최소 권한으로 실행되는데, 최소 권한에는 루트 폴더가 없어요. 읽기에서는 기본 user보다 약해요. 그런 작업은 내 홈 폴더에서 보호된 경로가 아닌 파일은 무엇이든 읽을 수 있지만(예를 들어 내 다른 프로젝트), user의 작업은 자기 루트 폴더 안에서만 읽어요. 최소 권한으로 실행되는 작업은 /tmp/aipager-files/에 있는 모든 채팅의 업로드도 읽을 수 있는데, user의 작업은 자기 채팅의 업로드만 읽어요. 쓰기는 여전히 세션의 폴더와 scratchpad로 제한돼요. 멤버의 세션에서 온 확인된 메시지는 그 멤버의 루트 폴더를 함께 가져와요.
  • 루트 폴더는 파일 도구만 묶어요. 내가 Bash를 준 직접 만든 역할은 shell로 루트 폴더 밖을 읽을 수 있어요(기본 user와 read_only에는 Bash가 없어요). bypass_safety는 있지만 can_manage가 없는 직접 만든 역할은 파일 도구의 경로를 빼면 모든 것에 건너뛰기 권한을 유지해요. 어떤 도구나 명령 패턴도 그 역할에 금지되지 않아요.
  • 다른 멤버의 루트 폴더를 적은 거부 이유는 그 폴더의 경로를 Claude에게, 그리고 채팅에 드러내요.
  • 루트 폴더가 생겼을 때(업그레이드) 이미 실행 중이던 멤버의 작업은 그 멤버의 다음 메시지까지 루트 폴더가 없어요.
  • 프로젝트 안에서 제한된 사용자는 내가 나중에 직접 실행하거나 읽는 것을 바꿀 수 있어요(소스 코드, Makefile, package.json의 스크립트, conftest.py, 내 작업이 불러오는 CLAUDE.md).

Bash가 있는 역할(역할 금지 규칙을 건너뛰는 admin, 또는 policy.yaml이 Bash를 돌려준 역할: Bash를 적은 allow_tools, 또는 바꾼 deny_tools)은 최선의 노력인 규칙에만 묶여요. 아래의 명령 패턴이고, 마음먹은 사람은 피해 갈 수 있어요. aipager doctor는 멤버가 가진 그런 역할마다 한 번 경고해요. 이 컴퓨터의 shell을 줘도 되는 사람에게만 Bash를 주세요.

기본 명령 규칙. Bash가 있는 소유자 외의 모든 Telegram 작업이 따라야 하는 Bash 패턴 가운데 하나는 claude라는 단어를 막아요. 규칙 하나로 중첩된 claude(경로, 감싸는 명령, 옵션이 무엇이든)와 ~/.claude에 대한 모든 접근을 막고, 이름이 같은 접두사로 시작하는 aipager 자체의 /tmp/claude-* 제어 파일의 이름도 잡아요(이 파일들에 대해 패턴은 진짜 보호가 아니에요. glob 작성법으로 피할 수 있어요). 유일한 예외는 Claude Code가 만드는 scratchpad 폴더 /tmp/claude-<uid>/…예요. 바로 그 폴더가, 경로의 한 부분 전체로서, 예외라서 세션의 scratchpad에서 실행하는 명령은 멈추지 않아요. 이 폴더는 세션 하나가 아니라 운영 체제 사용자의 것이라서, Bash가 있는 작업은 거기 있는 다른 세션의 scratchpad와 작업 출력을 읽을 수 있어요. Read 도구도 그럴 수 있는 것처럼요. 그것의 glob이나 비슷하게 생긴 이름(/tmp/claude-1*, claude-1000x), 다른 임시 폴더, 거기서 닿을 수 있는 모든 것(…/../../home/you/.claude)은 여전히 막혀요. 이 단어의 다른 쓰임(git log --grep=claude, claude-notes.md라는 파일, pip show claude-agent-sdk)도 일부러 여전히 막아요. 같은 단어가 프로그램, 그 SDK, aipager의 제어 파일 이름이고, 패턴은 이것들을 구별할 수 없으니까요. 내가 policy.yaml에 더한 규칙은 scratchpad를 포함해 입력된 그대로의 명령을 봐요.

Bash로 하는 검색. 어떤 Claude Code 버전에는 Grep이나 Glob 도구가 없어서(2.1.289가 그래요), 그때 Claude는 Bash로 grep, rg, find를 써서 검색하고, 위의 Grep/Glob 규칙은 거기에 적용되지 않아요. owner를 뺀 모든 역할에 대해 명령 패턴은 홈 폴더 전체나 /를 재귀적으로 검색하거나 나열하는 것도 금지해요. grep -r(-R, --recursive, -d recurse, egrep, fgrep, rgrep), rg(그리고 rg --files), ag, ack(그리고 ack -f), find, fd, tree, du, ls -R로 다음 폴더를 다루는 것이에요: ~, ~user, $HOME, ${HOME} 또는 "$HOME", /home, /home/<user>(또는 /home/$USER), /Users, /Users/<user>, /root 또는 /(끝에 /, /., /*가 있든 없든), 그리고 aipager의 폴더가 있는 ~/.config나 ~/.local이에요. 이 규칙은 cd x && 뒤에서도, 파이프 안에서도, bash -c "..." 안에서도, xargs, env, timeout 뒤에서도, LC_ALL=C 같은 설정 뒤에서도 그대로예요. 답변은 프로젝트 폴더 안에서 검색하라고 알려 줘요. 프로젝트 폴더를 검색하는 것(grep -rn x ~/proj, rg x src/, find . -name y), 프로젝트 안에서 홈 경로라는 글을 찾는 것(grep -rn "$HOME" src, 옵션 다음 첫 단어는 폴더가 아니라 찾을 글이에요), 재귀적이지 않은 검색(grep x ~/.bashrc, ls ~)은 영향을 받지 않아요. 모든 명령 패턴처럼 이것도 최선의 노력이에요. 패턴은 단어를 볼 뿐 shell이 그것을 어디서 실행하는지는 보지 못해서, 홈 폴더에서(또는 그 아래에서) 실행하는, 폴더가 없거나 폴더가 .이나 ..인 검색(cd ~ && grep -r x .), 다른 변수나 glob으로 닿는 홈 폴더, /home, /Users, /root 밖에 전부 적은 홈 폴더를 보지 못하고, \grep도, -e나 -f로 끝나는 단어 바로 뒤의 폴더도 보지 못해요(-e/-f가 grep과 rg에 주는 글로 여겨져서 ag x -f ~는 통과해요). 어떤 옵션이 따로 값을 받는지도 몰라서, rg -t py "$HOME" .이나 grep -rn -A 3 "/home/you" src는 py나 3을 찾을 글로, 홈 경로를 폴더로 읽어서 거부돼요(-A3, --include=*.py로 쓰거나, 옵션을 찾을 글 뒤에 두세요). 옵션 묶음 안에서 -e에 준 홈 경로도 그래요(grep -rne "/home" src. -r -n -e는 괜찮아요). 다른 명령의 글 안에 따옴표로 들어간 명령(echo "a; rg x ~")도 거부되고, du -sh ~(크기만 봄)도 그래요.

차단 한 번이 왜 작업 전체를 멈추나요. 도구 호출 하나가 막히면 같은 작업의 이후 호출도 다음 메시지까지 모두 거부돼요. 이게 없으면 에이전트는 패턴을 빠져나가는 작성법이 나올 때까지 같은 행동을 다르게 써서(glob, 다른 명령, 인코딩한 경로) 그냥 다시 시도해요. 작업을 멈추면 첫 시도의 대가가 커져요. 그래도 패턴이 경계가 되지는 않아요. 처음 시도한 작성법이 바로 빠져나가는 것일 수도 있으니까요. 잘못 막았을 때의 대가는 멈춘 작업 하나이고, 그래서 위의 규칙들을 좁게 유지해요.

이 메모들이 담는 규칙이 사용자 쪽에서 어떻게 보이는지는 그룹 → 규칙이 작동하는 방식을 보세요.

Claude가 보내는 파일#

aipager가 시작하는 모든 세션에는 Claude Code의 SendUserFile 도구(CLAUDE_CODE_ATTACHMENTS_BY_PATH=1)가 있어요. 이 도구는 파일을 읽어 세션의 채팅으로 보내요(명령어). PreToolUse 훅은 Claude Code가 파일을 읽기 전에 호출의 모든 파일을 확인해요.

  1. 모든 작업에 적용되는 전송 하한선, owner의 작업과 터미널에서 입력한 메시지도 포함해요: aipager의 설정과 상태(~/.config/aipager/**, ~/.local/share/aipager/**, ~/.local/state/aipager/**와 /tmp의 제어 파일), ~/.claude/** 전체와 ~/.claude.json*(Claude의 자격 증명과 계정), 제한된 역할이 모두 접근하지 못하는 자격 증명 파일(~/.ssh/**, ~/.gnupg/**, ~/.aws/**, ~/.git-credentials, ~/.netrc와 그 목록의 나머지), /proc/**, /sys/**, /dev/**, 그리고 어디에 있든 비밀처럼 보이는 이름(.env, .env.*, *.pem, *.key, id_rsa*, id_ed25519*와 다른 키 이름들, *.p12, *.pfx, *.kdbx, *.jks, *.keystore, .pgpass, credentials, *credentials*.json). 경로를 쓰인 그대로, 그리고 실제로 가리키는 곳으로 확인하므로, 이런 파일을 가리키는 심볼릭 링크도 거부돼요. 무해한 파일 중에도 이런 이름이 있는데(Keynote의 *.key, .env.example), 이것들도 거부돼요.
  2. 파일 자체: 심볼릭 링크를 따라간 뒤 일반 파일이어야 하고, 하드 링크가 하나이며, 비어 있지 않고, 50MB 이하여야 해요.
  3. 작업 자체의 규칙: 작업이 읽을(Read) 수 없는 것, 그리고 멤버(can_manage가 없고 루트 폴더에 묶인 역할)에게는 더 엄격한 규칙이 적용돼요. 모든 파일이 쓰인 그대로도, 실제로 가리키는 곳도 루트 폴더 안에 있어야 해요. 멤버가 읽을 때와 달리 홈 폴더 밖의 파일(/tmp, /etc)이나 세션의 scratchpad에 대한 예외는 없어요.

거부는 부드러운 거부예요. Claude는 이유("SendUserFile refused, nothing was sent: ... Tell the person which file could not be sent and why.")를 읽고, 작업은 계속돼요. 도구가 차단될 때처럼 세션이 멈추지 않아요. 확인이 실패하면 owner라도 파일이 거부돼요. 거부된 파일마다 감사 로그에 FileRefused 기록이 남아요.

aipager는 파일에 관해서라면 제어 소켓의 말을 그대로 믿지 않아요. 어떤 로컬 프로세스든 소켓에 쓸 수 있고, 어떤 세션, 어떤 호출, 어떤 대화 기록이든 이름을 댈 수 있으니까요. 게다가 도구 호출의 hook 하나만으로도 대기 중인 세션이 작업 중으로 바뀌어요. 그래서 호출은 aipager가 프롬프트로 시작하는 것을 본 작업(aipager가 Telegram에서 입력한 프롬프트, 또는 UserPromptSubmit)과 그 작업의 백그라운드 작업 동안에만 인정되고, 세션이 아무 작업도 돌지 않는 대기 상태가 되면 끝나요. 그 시작 때 aipager는 시각과 그 세션에 대해 추적하는 대화 기록을 적어 둬요(같은 작업의 나중 UserPromptSubmit가 대화 기록을 다시 알려 줘요). aipager가 입력한 슬래시 명령은 작업을 시작하지 않아요(작업을 실행하는 명령은 자기 UserPromptSubmit를 직접 보내요). 시작되지 않은 작업(15초 뒤에도 세션이 작업 없이 대기 중)은 다시 닫혀요. Claude Code는 어떤 작업은 UserPromptSubmit 없이 시작해요. 작업이 도는 동안 보낸 메시지를 다음 작업으로 가져가는 경우와, Stop hook이 계속 이어 가게 하는 작업(/goal)이에요. 이런 작업은 그 메시지를 aipager가 직접 입력했거나, 적어 둔 대화 기록에 마지막 Stop 뒤에 작업이 시작됐다고 나오면(dequeue 큐 줄, 또는 Claude Code가 쓰는 Stop hook feedback: 줄) 인정돼요. 그 전에는 호출의 어떤 것도 읽지 않고, 최대 20초까지 기다려요. 파일 목록은 자기가 확인한 PreToolUse에서, 그것을 놓쳤을 때는(재시작 뒤) PostToolUse에서 가져와요. Claude Code의 결과는 절대 쓰지 않아요. 호출마다 한 번만 복사해요. 이미 복사한 호출 id는 다시는 복사하거나 보내지 않아요(세션마다 마지막 256개를 재시작 뒤에도 기억해요). 파일마다 실제 경로를 딱 한 번 찾아서 그 경로에서 다시 확인하고(하한선, 파일 확인, 세션의 루트 폴더), 링크를 따라가지 않고 바로 그 경로를 열어, 연 파일이 확인한 그 파일이고 아직 그 경로에 있는지 확인한 뒤, ~/.local/state/aipager/outbox/<chat>/(폴더 0700, 파일 0600)에 복사해요. 이곳은 제한된 작업이 원래 접근하지 못하는 곳이에요. 보내기 전에는 그 작업에 대해 적어 둔 대화 기록에서 호출을 찾고(서브에이전트의 호출이라면 그 옆에 있는 그 에이전트 자신의 대화 기록에서), 도구 호출의 데이터그램이 가리키는 기록에서는 절대 찾지 않아요. 그 id와 정확히 그 파일들을 가진 SendUserFile tool_use가 담긴 어시스턴트 줄이, 같은 폴더에서, 작업 시작보다 이르지 않게 쓰여서, ~/.claude/projects(또는 aipager 자신에게 설정된 $CLAUDE_CONFIG_DIR/projects) 아래 aipager 자신의 OS 사용자가 소유한 .jsonl 파일에 있어야 해요. Claude Code는 대화 기록을 늦게 쓰기 때문에 aipager는 호출을 최대 20초까지 기다리고, 세션의 작업이 아직 돌고 있으면(백그라운드 에이전트, 도구 호출) 파일을 남겨 두었다가 나중에 다시 찾아요. 아무것도 돌지 않을 때까지도 찾지 못한 호출은 보내지 않고 사본을 지워요. 그런 작업이 돌지 않을 때 온 호출이나 두 번째로 온 호출은 아예 복사하지 않아요. 이런 호출의 파일마다 감사 로그에 이유가 unverified인 FileFailed 기록이 남아요. Claude 자신의 대화 기록에 작업 중 바로 그 호출이 있을 때만 채팅에 한 줄이 가요(⚠️ report.pdf 파일을 보내지 못했어요: Claude가 이번 작업에서 보낸 파일인지 aipager가 확인하지 못했어요.). 그 줄에는 대화 기록의 그 줄이 적은 파일 이름이 나와서, 꾸며낸 호출은 채팅에 줄도 이름도 넣을 수 없어요. 그 밖에는 채팅에 아무것도 안 보내요. 사본만, 세션의 채팅으로만, 작업의 답변 다음에, 그 세션의 앞선 파일들 다음에, 다음 작업을 붙잡지 않고 보내며, 보낸 뒤 바로, 아니면 하루 뒤에 지워져요.

이 확인으로 막지 못하는 것: aipager 자신의 OS 사용자로 실행되는 프로세스는 소켓뿐 아니라 Claude의 대화 기록도 쓸 수 있어요(그 파일들은 원래 직접 읽을 수도 있어요). 소켓에 닿을 수 있는 다른 사용자의 프로세스는 UserPromptSubmit도 꾸며낼 수 있어요. 그러면 이 확인에서는 작업이 시작된 것으로 보고, 그 작업의 대화 기록도 그 프로세스가 정해요. 그 뒤에 그 순간 이후 같은 폴더에서 그 대화 기록에 쓰인 진짜 SendUserFile 호출이 있다면(예를 들어 바로 그때 다른 세션이 하고 있는 호출), 그 호출이 가리킨 파일을 aipager가 그 세션의 채팅으로 보내게 할 수 있어요. 호출마다 한 번이에요. 그러려면 지금 진행 중인 그 호출의 id(tool_use_id)도 보내야 하는데, 다른 사용자는 그것을 읽을 수 없어요. 그 id는 aipager 사용자의 ~/.claude 아래 대화 기록에 있고, hook은 그것을 명령줄이 아니라 입력으로 받아요. 꾸며낸 Stop은 이 확인에서 작업을 끝내기 때문에, 그 작업이 그 뒤에 보내는 파일은 보내지지 않아요. aipager와 다른 $CLAUDE_CONFIG_DIR로 시작한 세션이나, aipager가 UserPromptSubmit를 놓친 작업(aipager가 재시작하는 동안 터미널에서 입력한 작업)은 파일을 보내지 않아요. Claude Code가 다른 방식으로 hook 없이 시작한 작업도 파일을 보내지 않아요(알려진 것은 위의 두 가지예요). 그 작업에서 Claude가 요청했다고 대화 기록에 나오는 파일은 위의 줄을 받아요. 소켓은 $XDG_RUNTIME_DIR가 설정되어 있으면 다른 사용자가 닿을 수 없는 그곳에 있고, 그렇지 않으면 /tmp에 있어요.

감사 로그#

허용 / 거부를 누를 때마다, 그리고 AskUserQuestion에 답할 때마다 ~/.claude/aipager-audit.jsonl에 JSON 한 줄이 추가돼요:

json
{
  "ts": "2026-05-18T15:42:11+00:00",
  "session": "claude-jim",
  "label": "jim",
  "action": "Allowed",
  "tool": "Bash",
  "summary": "ls -la /tmp",
  "user_id": 12345,
  "username": "alice",
  "denied": false
}

필드(전체 목록은 aipager/audit.py 참고):

  • ts: ISO 8601 형식의 UTC 타임스탬프, 초 단위까지.
  • session / label: 내부 이름과 읽기 쉬운 이름.
  • action: 무슨 일이 있었는지(Allowed, Denied, Allowed always, AskUserQuestion 답변, 자동 거부 등).
  • tool / summary: 도구 이름과 잘라 낸 입력, 또는 질문 내용.
  • user_id / username: 누가 눌렀는지, 팀 모드에서 누가 무엇을 했는지 알 수 있게. 채팅이 여러 개 설정되어 있으면 scope_label / scope_chat_id도 있어요.
  • denied: 누름 때문이든 규칙 때문이든 모든 거부에서 true.

쓰기는 최선의 노력이에요. 디스크가 가득 찼거나 ~/.claude/에 쓸 수 없으면 aipager는 로그에 WARNING을 남기고 계속 실행돼요. 조용히 사라지는 것도, 비정상 종료도 없어요. aipager/audit.py를 보세요.

디스크의 감사 로그는 추가만 돼요. 채팅의 감사 답변(결정마다 Telegram 메시지 하나, 작업 카드 아래)과 함께 서로 독립된 기록 두 개가 생겨요.

권한 경계#

aipager는 절대 권한을 높이지 않아요. sudo도, setuid도, doas도 없어요. aipager가 쓰는 파일은 모두 내 홈 폴더 안, /tmp 안(aipager의 세션마다의 제어 파일, 다운로드한 파일, 재시작 로그), 또는 제어 소켓이 있는 폴더($XDG_RUNTIME_DIR) 안에 있어요. 전체 목록은 아키텍처에 있어요. 모든 자식 프로세스(claude, dtach, 설치 프로그램, claude update)는 aipager의 사용자로 실행돼요.

Telegram에서 음성 메시지 기능을 설치하는 것([📦 음성 메시지 기능 설치])은 aipager를 설치한 설치 프로그램을 거치고(uv tool install --reinstall aipager[voice], pipx install --force aipager[voice]), 그게 안 되면 aipager 자체의 Python으로 이 기능의 패키지를 설치해요(<python> -m pip install). 항상 aipager 자체 환경에 설치하고, 시스템 Python에는 절대 설치하지 않아요.

[🔄 aipager 재시작] 버튼은 systemd 서비스에서는 별도의 임시 사용자 서비스 안에 systemctl --user restart aipager.service를 예약해요. macOS에서는 launchctl kickstart -k를 실행해요. 내가 직접 시작한 aipager는 자기 자신의 분리된 사본을 만들고, 그 사본이 예전 것이 끝나기를 기다려요. 업데이트 뒤에는 systemd 서비스만 저절로 재시작해요(명령 → 업데이트 참고). 모두 같은 사용자로 실행돼요. 권한을 높이는 일은 없어요.

네트워크 연결#

aipager는 루프백 밖의 TCP 포트를 하나도 듣지 않아요. Mini App 서버는 127.0.0.1에만 bind하고(코드에 고정되어 있어요. 일부러 host 옵션이 없어요), 아래에서 설명하는 관리형 터널로만 밖에서 닿을 수 있어요. 밖으로 나가는 연결:

  • api.telegram.org로의 HTTPS long polling(Telegram 봇이 업데이트를 받아요).
  • Mini App SDK 스크립트(telegram-web-app.js)를 받으려고 telegram.org로 HTTPS. Mini App 서버가 시작할 때 한 번 받고, 최대 하루에 한 번 새로 받으며, ~/.local/share/aipager/webapp-sdk/에 캐시해요. aipager는 이 바이트를 페이지 자신의 출처에서 페이지에 제공하고, 직접 실행하는 일은 절대 없어요. aipager는 이 스크립트를 재배포하지 않아요. 패키지에 들어 있지 않고, aipager에 사본이 없으면 페이지는 예전처럼 telegram.org에서 바로 불러와요.
  • Mini App이 켜져 있는 동안(기본으로 켜져 있어요) Cloudflare로의 터널. 터널이 실행하는 cloudflared 프로그램은 github.com/cloudflare/cloudflared 릴리스에서 한 번 받고, 고정된 SHA-256으로 확인하고, ~/.local/share/aipager/cloudflared/에 캐시해요. 내 PATH에 있는 cloudflared는 절대 쓰지 않아요. 내 Mini App 주소를 쓰면 이 가운데 아무 일도 일어나지 않아요(아래 참고).
  • 누군가 업데이트 확인이나 업데이트를 누를 때(/update, Mini App의 업데이트 부분), 또는 업데이트할 수 있는 사람이 Mini App의 설정 탭을 열 때(열린 앱마다 최대 10분에 한 번)만 하는 버전 확인: aipager는 pypi.org, Claude Code는 설치 방식에 따라 downloads.claude.ai나 registry.npmjs.org. 그다음 업데이트는 설치 프로그램(PyPI나 Homebrew와 통신)과 claude update를 실행해요. 명령줄의 aipager update는 설치 프로그램만 실행해요.
  • 음성 메시지 기능 설치, 그 버튼을 누를 때: 설치 프로그램이 PyPI에서 패키지를 받고, 음성 인식 모델은 처음 쓸 때 Hugging Face(huggingface.co)에서 받아요.
  • raw.githubusercontent.com(신고 키 파일)과 Sentry의 수신 서버로 HTTPS, 문제 신고 보고서를 보내겠다고 내가 확인했을 때만: 내가 미리 본 보고서와 그것으로 만든 짧은 요약이고, 다른 것은 없어요. 문제 신고를 보세요.

안으로 들어오는 연결:

  • $XDG_RUNTIME_DIR/aipager.sock의 Unix datagram 소켓($XDG_RUNTIME_DIR이 설정되지 않으면 /tmp/aipager.sock으로 돌아가요). aipager가 시작할 때 bind하고 chmod로 권한을 정해요(aipager/dtach/hook_receiver.py). 권한 0o666이라서 이 컴퓨터의 어떤 프로세스든 hook 이벤트를 보낼 수 있어요. 무엇이 hook을 실행할지 이미 정하는 ~/.claude/settings.json과 같은 신뢰 수준이에요.

즉 aipager는 원격 공격 표면이 아니에요. 네트워크의 공격자는 먼저 이 컴퓨터에 발판을 마련하지 않으면 닿을 수 없어요.

Claude의 글과 Telegram이 스스로 하는 다운로드. Telegram 서버는 메시지 안의 어떤 주소들을 스스로 내려받아요. 서식 있는 메시지 안의 이미지, 동영상, 오디오 블록, 그리고 링크 미리 보기 뒤의 페이지예요. Claude의 답변, 작업 카드에 적는 메모, 에이전트 설명, 도구 인수는 Claude가 쓴 그대로 Telegram에 가니까, 프롬프트 인젝션으로 Claude가 비밀을 밖으로 실어 나르는 주소를 쓰게 만들 수 있어요. 도구 호출도 권한 요청도 없이요. aipager는 첫 번째 길을 막고, 두 번째 길은 Telegram에 끄는 스위치가 있는 곳에서 막아요:

  • 모든 서식 있는 메시지에서 ![alt](url)로 쓴 이미지는 alt 글이 붙은 일반 링크로 보내고, 다른 이미지 문법과 그대로 쓴 <img>, <video>, <audio>, <source>, <picture>, <figure>, <iframe>, <embed>, <object>, <tg-document>, <tg-collage>, <tg-slideshow>, <tg-button>, <tg-button-row> 태그는 글로 보여 줘요(aipager/bot/media_guard.py). HTML 조각 같은 코드 블록 안에서는 그 때문에 \나 &lt;가 하나 더 보여요.
  • aipager가 보내거나 편집하는 모든 일반 텍스트와 HTML 메시지(답변과 카드의 일반 텍스트 대체 메시지 포함)에서 링크 미리 보기는 꺼져 있고, aipager 자신의 메시지도 마찬가지예요. Mini App 링크는 버튼이라서 Telegram이 가져오지 않아요.

Telegram은 서식 있는 메시지의 미리 보기 옵션을 문서에 적어 두지 않았어요. 서식 있는 메시지에 Telegram이 링크 미리 보기를 만드는지는 실제 서비스에서 확인하지 않았어요.

Mini App 터널#

Mini App은 기본으로 켜져 있어서, 표준 설치에서 aipager는 아무도 요청하지 않아도 이 터널을 열어요. 의도한 맞바꿈이지만(직접 켜야 하는데 아무도 발견하지 못하는 옵션은 없는 기능과 같아요), 새 설치가 인터넷에 연결된다는 뜻이에요. aipager miniapp disable로 끄거나, aipager.yaml의 miniapp: 아래에 enabled: false를 설정하세요. 분명히 적은 false는 항상 따라요. 참고로 빠졌거나 망가진 miniapp: 블록은 이제 꺼짐이 아니라 켜짐으로 읽혀요.

Mini App은(--url로 대신하지 않는 한) 관리형 Cloudflare quick tunnel을 aipager 자체의 루프백 서버로 시작하고, 받은 공개 주소 https://*.trycloudflare.com을 Telegram 메뉴 버튼과 /app의 답변으로 올려요.

이 주소는 비밀이 아니고, 비밀일 필요도 없어요. Mini App이 처리하는 모든 요청은 Telegram의 initData 서명으로 확인하고(위의 신뢰 경계 참고), 주소를 어떻게 얻었든 그것을 지키는 것은 이 같은 확인이에요. 서버 이름을 아는 것만으로는 trycloudflare.com 하위 도메인을 아무렇게나 짐작하는 것보다 공격자에게 나을 게 없어요. 서명 확인이 진짜 문이에요. 공개하지는 않지만 비밀도 아닌 다른 주소처럼 다루세요. 계속 실행해도 괜찮지만, 이유 없이 공개 채널에 붙여 넣지는 마세요.

서버 이름은 메모리에만 있어요. aipager.yaml이나 ~/.config/aipager/ 아래 어디에도 절대 쓰지 않아요. 계획된 재시작(업데이트 후 재시작, 지금 재시작, 서비스 없이 제자리에서 하는 재시작)은 터널을 그대로 켜 둬서, 주소와 메뉴 버튼과 열려 있는 Mini App이 계속 동작해요. 다음 aipager는 같은 부팅인지, 같은 포트인지, 그 프로세스가 바로 그 cloudflared인지(명령줄로 확인), 아직 같은 주소를 알려 주는지(~/.claude/aipager-tunnel.json에는 주소가 아니라 주소의 해시만 있어요), 그 주소가 응답하는지 확인한 뒤에만 터널을 이어받아요. 그 밖의 경우에는 터널을 멈추고 새로 시작하고, 3분 안에 아무 aipager도 이어받지 않은 유지된 터널은 작은 감시 프로세스가 멈춰요. 다른 방식으로 멈추면(Ctrl+C, docker stop, systemctl stop) 터널도 멈추고, 컴퓨터를 재부팅하거나 컨테이너를 다시 시작하면 주소가 새로 생겨요. MINIAPP_KEEP_TUNNEL=0으로 터널 유지를 끌 수 있어요. aipager가 강제로 종료되면(메모리 부족, kill -9) aipager가 관리형 터널을 켠 채(Mini App 사용, MINIAPP_PUBLIC_URL 없음) 다시 시작해서 터널을 이어받거나 멈출 때까지, 또는 컴퓨터를 재부팅할 때까지 터널이 계속 실행돼요. aipager uninstall도 터널을 멈춰요. 터널이 도중에 끊기면 aipager는 점점 길어지는 간격으로 다시 시작하고 새 주소를 다시 올려요. 몇 번 시도해도 살아나지 않으면 죽은 주소를 가리키게 두지 않고 버튼을 없애요.

MINIAPP_PUBLIC_URL(또는 aipager miniapp enable --url https://…)을 설정하면 관리형 터널이 완전히 꺼지고(cloudflared 프로그램을 받거나 실행하는 일이 절대 없어요), 준 주소를 그대로 쓰며, 그 뒤에서 같은 initData 확인을 해요. 이미 Tailscale을 쓰고 있고 Cloudflare에 전혀 기대고 싶지 않은 사람에게는 Tailscale 자동 감지(tailscale status --json)가 여전히 더 간단한 선택이에요.

음성 메시지를 글로 바꾸기#

faster-whisper가 프로세스 안에서 실행돼요. 음성은 .ogg로 /tmp/aipager-files/ 아래의 그 채팅 자신의 폴더(올린 파일이 가는 곳, 0700, 파일은 0600)에 내려받고, 이 컴퓨터의 CPU로 글로 바꾸며, 파일은 계속 내 손에 있어요. 음성은 하나도 이 컴퓨터를 떠나지 않아요. 외부 API도 없어요. Telegram과 대화하려고 이미 필요한 봇 token 말고는 어떤 키도 필요 없어요.

글로 바꾼 뒤 .ogg 파일을 지우면, 이 메시지의 유일한 글 기록은 claude 세션에 입력된 받아쓴 글이에요(거기서는 Claude Code의 개인정보 처리방침을 따라요).

세션 사이의 격리#

Claude Code 세션은 각자 자기 dtach에서 실행돼요. /tmp/claude-dtach-<name>.sock의 제어 소켓은 aipager의 사용자 소유이고, dtach는 다른 사용자의 연결을 거부해요. 세션은 각자 자기 폴더에서 시작하는데, 그 폴더는 /new나 Mini App에서 고르거나, 내가 aipager session을 실행한 폴더예요.

aipager는 파일 시스템 수준에서 세션을 격리하지 않아요. ~/projects/foo에 묶인 세션도 claude code의 권한이 허락하면 원칙적으로 ~/projects/bar를 읽을 수 있어요. 더 엄격한 격리가 필요하면 프로젝트마다 따로 ~/.claude/settings.json을 쓰거나 컨테이너(Docker 이미지)를 쓰세요.

Claude 세션 사이의 메시지#

Claude Code는 세션 하나가 다른 세션에 메시지를 보내게 해 줘요(SendMessage 도구). 받는 세션에서 이 메시지는 다른 메시지와 똑같으니, aipager는 Telegram 메시지의 권한을 정하는 것과 같은 방식으로 이 메시지의 권한을 정해요. 메시지 내용이 무엇이라고 하는지가 아니라, 누가 정말로 보냈는지에 따라서요.

  • 메시지는 보낸 사람의 권한을 가지고 가고, 그보다 많이는 절대 아니에요. aipager가 세션이 메시지를 보내도록 허락하면 한 번만 쓰는 영수증을 써요. 그 정확한 글의 지문, 보낸 Claude 프로세스(프로세스 번호와, Linux에서는 시작 시각), 그 작업이 가진 권한이에요. Claude Code는 받은 메시지를 커널이 확인한 보낸 쪽의 프로세스 번호와 함께 기록하고, 보낸 쪽이 자기를 뭐라고 부르는지는 절대 믿지 않아요. 받는 작업이 어떤 도구든 실행하기 전에 aipager는 둘을 맞춰 보고, 그 작업에 보낸 작업과 정확히 같은 권한을 줘요. 소유자의 메시지는 소유자의 권한으로, 멤버의 메시지는 그 멤버의 권한으로, 보낸 세션의 터미널에서 입력한 메시지는 제한 없이 실행돼요.
  • 영수증이 없으면 제한된 권한. aipager가 맞춰 볼 수 없는 메시지(aipager가 실행하지 않는 Claude 세션이나 프로세스가 보낸 것, 한 시간 뒤에 만료된 영수증, 다른 컴퓨터의 보낸 사람)는 가장 엄격한 규칙에 더해 파일 변경 금지와 웹 접근 금지로 실행돼요(읽고 생각하기만). 받는 세션 자신의 이전 권한은 절대 적용되지 않아요.
  • 세션 밖에서 온 다른 입력도 마찬가지예요. Claude Code가 이 세션의 사람에게서 오지 않았다고 기록하는 것은 모두 같은 제한된 권한을 받아요. MCP 채널의 푸시, 원격 호출, 플러그인이 대기열에 넣은 메시지, 다른 세션의 받은편지함에서 넘겨받은 메시지, aipager가 아직 모르는 모든 종류의 입력이에요(플러그인 명령은 내가 직접 시작했어도 바깥 입력으로 쳐요). 세션 자신의 백그라운드 에이전트(돌려주는 것과 보고)와 작업 알림은 그 일 전체의 권한을 유지해요.
  • 세션이 예약한 메시지는 그것을 예약한 작업의 권한으로 실행돼요. 작업이 /loop이나 cron을 설정하면 aipager는 그 메시지를 위해 그 작업의 권한을 기록해요. 나중에 실행될 때는 마지막에 누가 말했든 정확히 그 권한을 받고, 그보다 많이는 절대 아니에요. 그 작업에 합쳐지는 메시지는 좁힐 수만 있고, 예약한 사람의 권한을 줄이는 규칙 변경도 좁혀요. 같은 메시지를 두 번 예약하면 더 엄격한 권한을 유지해요. 내용 없는 /loop은 자기 글 대신 Claude Code의 반복 지시로 실행되니까, 저장된 대화에 그 실행이 보이는 순간 세션이 설정한 내용 없는 반복 가운데 가장 엄격한 권한을 받아요(그전까지는 거부된 도구 호출이 부드럽게 거부되고 Claude가 다시 시도해요). aipager의 확인을 거치지 않고 누군가 예약한 메시지는 제한된 권한을 받아요.
  • 글은 아무것도 증명하지 못해요. 메시지 안의 보낸 사람 이름, [via Telegram · @owner] 줄, “소유자가 요청하길…” 같은 말은 아무것도 바꾸지 않아요. Claude에게는 aipager가 누구를 보낸 사람으로 확인했는지, 그리고 글 속의 주장은 증거가 아니라는 것이 전해져요.
  • 실행 중인 작업에 합쳐지면 좁아지기만 해요. 작업이 실행되는 동안 다른 세션에서 온 메시지는 그 작업을 둘 가운데 더 엄격한 쪽만큼 엄격하게 만들고, 절대 느슨하게 만들지 않아요. Claude Code가 자기 메시지 이벤트 없이 메시지를 받아들일 때도 그래요(실행 중인 작업에 합쳐지거나, 대기열에서 다음 작업으로 실행되거나). 소유자의 빠른 경로는 도구를 호출하기 전마다 저장된 대화의 끝을 보고, 작업이 끝날 때와 aipager가 대기열에서 그런 메시지를 보면 세션에 확인 표시를 해요.
  • 역할은 자기 채팅에 머물러요. 다른 채팅의 세션에서 온 영수증은 그 작업이 소유자의 권한을 가졌거나 그 터미널에서 입력됐을 때만 인정돼요. 그 밖의 것은 모두 제한된 권한으로 실행돼요.
  • 누가 보낼 수 있나요. 기본 제한 역할은 SendMessage를 아예 쓸 수 없어요. 규칙이 SendMessage를 허용한 역할은 aipager가 실행하는 세션에만 보낼 수 있어요(uds:…/<pid>.sock 형태의 주소로). aipager doctor는 그런 역할에 대해 경고해요.
  • 백그라운드 에이전트는 자기 권한을 유지해요. 에이전트는 자기를 시작한 작업의 권한(또는 나중 작업의 더 엄격한 권한)을 유지하고, 권한이 더 많은 나중 작업의 권한은 절대 받지 않아요.
  • 멈췄을 때. 다른 세션이 시작한 작업이 안전 규칙 때문에 멈추면, 작업 카드는 이미 누가 보냈는지 보여 주고, 멈춤 알림도 그렇게 말하고, 일을 맡긴 사람도 알림을 받아서, 일이 조용히 사라지는 일은 절대 없어요. 두 세션이 같은 채팅에 있으면 보낸 세션의 채팅으로, 아니면 소유자에게만, 개인 채팅으로, 그것도 소유자(또는 터미널)가 보낸 작업을 다뤘을 때만 알려요.

알려진 한계:

  • shell이 있는 작업(소유자나 관리자의 것)은 aipager와 같은 운영 체제 사용자로 실행되니, aipager의 다른 제어 파일을 이미 다시 쓸 수 있는 것처럼 영수증도 위조할 수 있어요. 이것이 기존의 신뢰 모델이에요. 컴퓨터를 통째로 맡길 수 있는 사람에게만 shell을 주세요.
  • aipager가 실행하지 않는 Claude 세션은 누가 메시지를 보내든 aipager가 전혀 다루지 않아요.
  • Linux 밖에서는 보낸 쪽을 프로세스 번호만으로 맞춰요(거기서는 Claude Code가 시작 시각을 기록하지 않아요). Linux에서는 시작 시각이 없는 받은 기록은 아무것과도 맞지 않아요.
  • Claude Code는 작업의 첫 도구 호출보다 조금 늦게 메시지를 저장된 대화에 쓸 수 있어요. 쓰기 전까지는 제한된 권한이 거부할 호출을 부드럽게 거부해요(Claude에게 다시 시도하라고 하고, 작업은 멈추지 않아요). 8번 호출할 때까지 기록이 없으면 작업은 제한된 권한으로 실행돼요.
  • 소유자의 빠른 경로는 저장된 대화의 마지막 1 MiB를 읽고, 그보다 많이는 절대 읽지 않아요. 그보다 긴, 다른 세션에서 온 메시지는 빠른 경로가 그런 메시지로 여기는 줄로 끝나요(이 창에서 잘린 256 KiB 넘는 줄은 모두 그렇게 여겨요). 완전히 더 앞에 있는 메시지(다음 도구 호출 전에 그 뒤로 저장된 대화가 1 MiB 넘게 쓰인 경우)는 빠른 경로가 아니라, 작업이 실행되는 동안 aipager가, 또는 아직 대기열에 있을 때 작업이 끝나는 시점의 확인이 잡아요.
  • 세션에 내용 없는 /loop이 설정되어 있는 동안, Telegram 메시지가 시작하지 않은 작업(터미널에서 입력한 메시지 포함)은 처음 몇 번의 도구 호출이 부드럽게 거부될 수 있어요. Claude Code가 그 작업의 메시지를 쓰고 aipager가 그것이 반복 실행이 아님을 알 수 있을 때까지요. 그다음 Claude가 다시 시도해요.
  • aipager는 예약된 메시지의 작업을 작업이 시작된 뒤 저장된 대화에 늘어난 부분에서 읽어요. 8 MiB를 넘으면 처음과 마지막 4 MiB씩 읽어요(가운데 부분은 그것이 가장 새로운 부분이었을 때 이미 확인했어요).
  • 예약된 메시지를 위해 기록한 권한은 세션을 종료할 때(/kill)까지 남아요. 끝났거나 재개된 세션도 그것을 유지해요. Claude Code가 cron과 기다리는 /loop 깨우기를 유지하는 것처럼요. 세션마다 최대 256개를 보관하고, 그것을 넘어 또 예약하면 세션의 모든 사람에게 거부되니까, 예약할 수 있는 사람은 누구나 그것을 다 쓸 수 있어요(그러면 다른 이름으로 세션을 시작하거나 이 세션을 /kill하세요). 기록 없이 실행되는 메시지는 Claude Code가 저장된 대화에 쓴 순간 제한된 권한을 받거나, 내용 없는 /loop이 있는 세션에서는 그 반복의 권한을 받아요. 그전까지 aipager는 그 작업을 앞의 작업으로 읽어요. 이 버전 전에 설정한 메시지, 컴퓨터 재시작 뒤(기록은 시스템의 임시 폴더에 있고 재시작하면 지워지는데, Claude Code는 재개된 세션의 깨우기를 한 시간 안에 다시 가져올 수 있어요), 그리고 자기 세션이 끝난 뒤 Claude Code가 같은 폴더의 다른 세션에서 실행하는 오래 가는 cron에 이런 일이 생겨요. 권한이 더 적은 세션이 같은 글을 예약하면 메시지의 권한을 낮출 수 있어요(절대 높이지는 못해요).
  • 예전 세션의 이름으로 새로 시작한 세션(새 세션으로 교체)은 /kill할 때까지 그 세션의 기록을 유지하고, 내용 없는 /loop의 권한도 포함돼요.
  • Claude Code가 다른 작업 바로 뒤에 자기 메시지 이벤트 없이 실행하는 예약된 메시지(대기열에서)는 자기 권한과 뒤따르는 작업의 권한 가운데 더 엄격한 것을 받아요(터미널에서 입력한 작업 뒤라면 정확히 자기 권한이에요).
  • 터미널에서 입력한 작업의 처음 몇 번 도구 호출은 Claude Code가 그 메시지를 저장된 대화에 쓸 때까지 거부될 수 있어요(그때까지 aipager는 그 작업을 앞의 작업으로 읽어요).
  • 더 좁은 권한으로 시작한 백그라운드 에이전트는 같은 일의 나중 작업에 글을 돌려줄 수 있고, 그 작업은 Claude Code의 다른 작업 알림처럼 그 글을 일 전체의 권한으로 읽어요.
  • Claude Code는 받는 쪽 터미널에서 메시지를 승인 대기로 붙잡아 둘 수 있어요(crossSessionInbound 설정, 또는 두 세션이 다른 권한 등급에서 실행될 때, 예를 들어 전체 권한 모드와 자동 모드). aipager는 Telegram에서 그런 메시지를 풀어 줄 수 없고, 영수증은 최대 한 시간 기다려요.

위협 모델 요약#

위협대책
모르는 사람이 봇에 명령을 보냄채팅 ID 필터가 거부
봇 token 도난@BotFather에서 /revoke, 다시 설정
claude의 도구 호출이 악용됨aipager의 안전 검사가 Telegram 작업의 규칙이 금지하는 것을 거부해요. 확인 모드에서는 Claude Code 자체의 settings.json 규칙이 무엇을 물을지 정하고, 사람의 누름이 유일한 허용 방법이에요. 자동 모드(모든 새 세션의 기본값)에서는 Claude Code 자체의 안전 검사가 안전하다고 보는 호출은 묻지 않고 실행하고, 위험한 호출은 멈춰서 사람이 정하게 해요. 전체 권한 모드의 세션(관리자, 그리고 자기 개인 채팅 안의 사람)은 아무것도 묻지 않으니 aipager의 안전 검사만 길을 막고, 소유자 자신의 작업과 터미널에서 입력한 메시지에는 아무것도 막지 않아요(누가 정하나요)
제한된 Telegram 사용자가 소유자의 권한을 얻음shell 없음, 세션은 멤버 자신의 루트 폴더 안에서만 시작, 쓰기와 검색은 그 안의 프로젝트로 제한(홈 폴더는 절대 아님), 루트 폴더 밖의 홈 폴더는 읽지 못함, 다른 멤버의 루트 폴더와 다른 채팅의 업로드에는 닿지 못함, 로그인 정보 파일은 읽지 못함, 파일 도구의 보호된 경로, 고장 난 확인은 거부(팀 모드의 규칙 적용). Bash를 받은 역할에는 최선의 노력일 뿐이고, “알려진 한계”의 경우는 다루지 않아요
세션의 메시지가 소유자의 권한을 주장함메시지는 보낸 작업의 증명된 권한으로 실행되고, 받는 세션의 이전 권한으로는 절대 실행되지 않아요. 증명이 없으면 읽고 생각하기만 해요(세션 사이의 메시지)
그룹 멤버가 다른 사람의 권한을 빌림메시지마다 그것이 온 채팅에서 보낸 사람 자신의 역할로 실행돼요. 다른 보낸 사람의 메시지는 실행 중인 작업을 기다려요. 그래도 합쳐지면 가장 엄격한 규칙이 이겨요. 제한된 역할의 슬래시 명령은 키보드의 명령으로만 제한돼요(팀 모드의 규칙 적용)
그룹의 잡담이 세션으로 들어감그룹에서는 명령, 봇에 대한 답장, 봇 멘션, 표시가 있는 키보드 누름만 처리해요(그룹)
지워진 멤버가 여전히 접근 권한을 가짐aipager config가 재시작 없이 aipager를 다시 불러와요. 그 사람의 보류된 메시지는 버려지고, 아직 실행 중인 작업은 예전 권한을 잃어요. Telegram 그룹에서 그 사람을 내보내면 더 이상 그룹을 읽을 수 없어요(그룹 → 재시작 없이 다시 불러오기)
감사 로그 조작추가만 해요. 별도의 서명 프로그램 없이 완전히 막는 것은 범위 밖이에요
프롬프트 인젝션으로 Claude가 데이터를 밖으로 실어 나르는 주소를 씀Claude의 글 속 이미지와 미디어 태그는 미디어가 아니라 링크나 글로 보여 주고, 일반 텍스트와 HTML 메시지의 링크 미리 보기는 꺼져 있어요(네트워크 연결)
네트워크의 공격자들어오는 포트가 없고, 직접 닿을 수 없어요
이 컴퓨터에서의 권한 상승sudo / setuid 없음. aipager는 사용자 공간에 머물러요
음성 메시지 소리가 클라우드로 새어 나감받아쓰기는 이 컴퓨터에서 해요
에이전트가 aipager 자체의 Claude 로그인 정보를 읽음예상된 일이고 버그가 아니에요. Claude 로그인 정보를 보세요. 그것이 든 파일이 아니라 로그인 정보 자체를 제한하세요

함께 보기#