IceCmd — 오르카가 무거워서 직접 만든 경량 터미널 매니저 2026.09.03

죽은 프로그램이 켜 놓고 간 마이크 — IceCmd 0.6.5

claude를 끊었더니 셸 입력줄에 마우스 좌표가 줄줄이 찍혔다. 범인은 IceCmd가 아니었지만 뒷정리는 IceCmd 몫이었다. 고치는 김에, 고치던 세션이 스스로를 죽인 이야기도.

IceCmd 0.6.5를 냈다. 고친 것은 하나인데, 그 하나를 고치는 동안 고치던 세션이 스스로를 죽였다. 그 얘기까지 적는다.

셸이 마우스 좌표를 받아쓰고 있었다

claude를 띄운 칸에서 작업하다가 끊었다. 그 뒤로 남은 cmd 입력줄에 이런 것이 줄줄이 찍혔다.

C:\Users\iceno>^[[I^[[O^[[<35;79;20M^[[<35;65;29M^[[<35;62;31M^[[<0;58;41M^[[<0;58;41m

마우스를 움직일 때마다 늘어났다. 지우면 다시 찍혔다. 칸을 닫는 것 말고는 방법이 없었다.

찍힌 것의 정체는 이렇다.

  • ^[[I / ^[[O — 창에 포커스가 들어왔다/나갔다는 보고
  • ^[[<35;79;20M — 마우스가 79열 20행으로 움직였다는 보고. 35는 32(이동)+3(버튼 없음)
  • ^[[<0;58;41Mm — 58열 41행에서 왼쪽 버튼을 눌렀다/뗐다

터미널이 "마우스 움직임과 창 포커스를 전부 보고하라"는 모드에 걸려 있고, 그 보고를 받는 쪽이 cmd.exe다. cmd는 그게 뭔지 모르니 키 입력으로 받아 그대로 화면에 되뱉는다.

비유하면 회의실에 무선 통역기를 켜 둔 채 통역사가 나가 버린 상태다. 송신기는 계속 신호를 쏘고, 받을 사람이 없으니 그 신호가 회의록에 날것 그대로 적힌다.

범인은 CLI지만 뒷정리는 우리 몫이다

모드를 켠 것은 claude다. claude와 codex는 마우스·포커스 보고를 켜고 시작해서, 나갈 때 끈다. 정상적으로 나갈 때만. Ctrl+C로 끊거나 크래시로 죽으면 끄지 못한 채 나간다.

다른 터미널이라면 여기서 끝이다. 프로세스가 죽으면 칸도 같이 닫히니까. 그런데 IceCmd는 CLI 칸을 cmd /K … claude로 띄운다. CLI가 나가도 쓸 수 있는 셸이 남게 일부러 그렇게 했다. 좋은 설계라고 지금도 생각하는데, 그 대가로 "모드를 켠 앱은 죽었고 모드는 켜진 채 남은" 구간을 IceCmd가 직접 만들어 내는 셈이 됐다.

더 나쁜 건 사용자가 풀 방법이 없다는 것이다. 리눅스면 reset 한 번이면 되는데 cmd.exe에는 그런 명령이 없다.

그래서 둘을 넣었다.

우클릭 메뉴에 「입력 모드 초기화」. 보고 모드만 끈다. term.reset()을 쓰면 간단하지만 그러면 화면과 스크롤백이 날아간다. 실수로 누를 수 있는 메뉴 항목이 화면을 지워서는 안 된다.

자동 복구. 이쪽이 본편이다. cmd.exe는 받은 이스케이프를 그대로 되뱉는다. 그러니 방금 우리가 내보낸 마우스 보고와 똑같은 바이트가 출력으로 돌아온다면, 그건 받아먹는 프로그램이 없다는 확실한 신호다. 키 입력이 되돌아오는 건 셸이 늘 하는 일이라 아무 뜻도 없지만, 마우스 보고가 되돌아오는 건 결코 정상일 수 없다. 그 순간 모드를 끄고, 왜 껐는지 회색 한 줄로 적는다. 첫 한 줄만 지저분해지고 그 뒤로는 안 나온다.

ESC가 아니라 ^[ 두 글자였다

구현하고 단위 검사를 35개 통과시킨 뒤에야 알았다. 화면에 보이는 ^[는 ESC 문자가 아니다. cmd가 ESC를 ^[ 두 글자로 바꿔서 내보낸 것이다. 그래서 화면에 코드가 보이는 것이다. 진짜 ESC였으면 터미널이 해석해서 아무것도 안 보였을 테니까.

원본 바이트로만 찾으면 영영 못 찾는다. 검사는 통과하고 기능은 죽어 있는, 이 프로젝트에서 세 번째쯤 겪는 함정이다. 캐럿 표기로도 찾게 고쳤고, 하네스도 cmd가 실제로 되뱉는 모양 그대로 흘려보내게 바꿨다.

고치던 세션이 스스로를 죽였다

여기까지 고치고 하네스를 돌리던 Claude 세션이 갑자기 끊겼다. 20분 작업이 통째로 사라진 줄 알았다.

원인은 세션 기록 마지막 줄에 있었다. 하네스 뒷정리로 개발 빌드를 죽이려고 taskkill /F /IM icecmd.exe를 쳤다. 그런데 그 세션은 IceCmd 안에서 돌고 있었다. 이 앱은 이 앱 안에서 개발한다. 개발 빌드와 설치본은 프로세스 이름이 같고, 이름으로 죽이는 명령은 둘을 구분하지 못한다. 옆집 불을 끄겠다고 소방서에 자기 집 주소를 부른 격이다.

코드는 디스크에 살아 있었다. 다음 세션이 그것을 이어받아 검증하고 냈다. 배운 것은 단순하다. 자기가 올라타 있는 프로그램을 이름으로 죽이지 마라. 경로로 골라 PID로 죽인다. 그리고 이 교훈은 메모리에 적어 뒀다. 같은 함정에 두 번 빠지는 것까지 봐줄 수는 없으니까.

하네스에서 또 배운 둘

끊긴 세션이 남긴 첫 하네스 결과는 182개 통과, 2개 실패였다. 둘 다 이번 수정과 무관한 검사였다.

하나는 "일반 셸에는 색을 강제하지 않는다"는 검사였다. 실패 이유는 앱이 아니라 하네스 자신에게 있었다. 그 검사는 스위치를 잠깐 켜고 끝에 되돌리는데, 죽은 실행에는 끝이 없다. 켜진 채 남은 스위치를 다음 실행이 읽었다. 이제 시작할 때 그 잔재를 스스로 치운다.

다른 하나는 "where claude가 PATH에서 찾힌다"는 검사였다. 손으로 치면 멀쩡히 나온다. 다만 where는 PATH의 모든 폴더를 훑고, 그중에 네트워크 드라이브가 있으면 처음 한 번은 2초를 넘긴다. 검사는 0.9초를 기다리고 화면을 읽었다. 고정 대기를 폴링으로 바꿨다.

둘 다 이 프로젝트에서 반복해서 배우는 것과 같다. 검사가 실패하면 대상보다 먼저 검사 도구를 의심하라.

이번 릴리스는 184개 통과 / 0 실패 / 건너뜀 3으로 나갔다. 건너뛴 셋은 창에 포커스가 없으면 못 하는 클립보드 왕복이다.


정리하면, 이번에 고친 것은 우리가 만든 버그가 아니다. 남이 켜 놓고 간 것을 우리가 끄는 일이다. 그런데 사용자에게는 그 구분이 아무 의미가 없다. 화면에 이상한 글자가 찍히는 곳이 IceCmd면, 그건 IceCmd 문제다.

내려받기 → icenovel.com/service/icecmd

IceCmd — 오르카가 무거워서 직접 만든 경량 터미널 매니저 프로젝트로