Skip to content

Dev Guide claude disable feedback

Woojin Kim edited this page Jun 20, 2026 · 1 revision

개발 가이드: claude-disable-feedback 플러그인

Claude Code 가 주기적으로 띄우는 "How is Claude doing this session?" 피드백 권유 문구를 화면에서 조용히 가리는 아주 작은 플러그인입니다. 단일 훅(server_filter_rows)만 쓰는 pytmux 플러그인 중 가장 작은 예제라, 이 문서는 "최소 플러그인" 워크드 예제(worked example)를 겸합니다. 훅 패턴을 처음 익힐 때 출발점으로 삼기 좋습니다.

목차

무엇을 하는가

Claude Code 는 두 가지 피드백 표면을 화면에 띄웁니다. 이 플러그인은 둘 다 폭을 유지한 채 공백으로 덮어 절대 보이지 않게 만듭니다.

  1. 시작 팁Tip: Use /feedback to help us improve!
  2. 세션 종료 배너How is Claude doing this session? (optional) 와 그 아래 평가 옵션 줄(1: Bad … 0: Dismiss)

"폭을 유지한 채 공백으로"가 중요합니다. 줄을 통째로 지우면 그 아래 행들이 위로 밀려 화면 레이아웃이 흔들립니다. 같은 폭의 공백으로 바꾸면 자리만 비고 레이아웃은 그대로라, 사용자 눈에는 "그 줄이 원래 없었던 것"처럼 보입니다.

UI 가 없다

이 플러그인은 자기 자신의 UI 가 전혀 없습니다. 팝업도, 상태줄 배지도, 키 바인딩도 만들지 않습니다. 하는 일은 오로지 "다른 무언가(피드백 문구)를 화면에서 제거"하는 것뿐이라, 보여줄 스크린샷도 없습니다. 정상 동작의 증거는 "아무것도 안 보임"입니다.

어떤 훅 하나를 쓰는가

이 플러그인이 구현하는 레지스트리 훅은 server_filter_rows 단 하나입니다.

def server_filter_rows(self, server, pane, rows):
    ...
    return rows

코어는 패널의 render 결과(행 목록)를 클라이언트로 전송하기 직전에 등록된 모든 플러그인의 server_filter_rows 로 흘려보냅니다. 각 플러그인은 행 목록을 받아 (변형하거나 그대로) 돌려주고, 코어는 그 결과를 다음 플러그인 또는 클라이언트로 넘깁니다. 즉 이 훅은 표시 직전 마지막 변형 지점입니다.

핵심 계약:

  • 이 플러그인이 없으면(디렉토리 삭제 또는 비활성) 코어는 행을 변형 없이 그대로 전송합니다. 코어는 플러그인을 직접 import 하지 않고 레지스트리 훅으로만 닿습니다.
  • 따라서 기능 전체가 이 디렉토리 한 곳에 담깁니다.

핵심 설계 결정: 키 주입이 아니라 표시 필터

피드백 배너를 없애는 방법은 두 가지를 생각할 수 있습니다.

  • (A) 키 주입 — 배너가 뜨면 패널에 Esc(또는 0: Dismiss 키)를 자동으로 보내 닫는다.
  • (B) 표시 필터 — 배너에 해당하는 render 행만 공백으로 덮어 표시만 가린다. 패널 상태는 건드리지 않는다.

이 플러그인은 (B) 표시 필터를 택합니다. 이유:

이 배너는 컴포저(입력 줄) 위에 뜨는 비모달(non-modal) 오버레이입니다. 닫지 않아도 작업을 막지 않습니다. 그런데 단일 Esc 를 주입하면 배너를 Dismiss 하는 대신 작동 중인 턴(busy turn)을 종종 interrupt 해버립니다. 키 주입은 화면 상태와 레이스(race)가 나서 사용자의 진행 중인 작업을 깨뜨릴 수 있습니다. 표시 필터는 패널의 실제 입력/실행 상태를 일절 건드리지 않으므로 안전합니다.

즉 "보이게 하지 않는다"는 목표를 상태 변경 없이 표시 레이어에서만 달성합니다.

코드 구조 따라 읽기

모듈은 ~90줄, textual/rich 같은 무거운 의존을 import 하지 않습니다(서버 프로세스도 같은 코드를 로드하고, 행 변형은 순수 문자열 연산뿐입니다). 구성은 세 덩어리입니다.

1) 시작 팁 가리기

# 문구 중 식별성 높은 부분으로만 매칭해 오탐을 피한다.
_FEEDBACK_TIP_MARK = "/feedback to help us improve"

def _blank_feedback_tip(rows):
    out = None
    for i, row in enumerate(rows):
        text = "".join(t for t, _ in row)
        if _FEEDBACK_TIP_MARK in text:
            if out is None:
                out = list(rows)
            out[i] = [[" " * len(t), st] for t, st in row]
    return out if out is not None else rows
  • 행 = 런(run) 목록: 각 행은 [text, style] 쌍(런)들의 목록입니다. 한 행은 스타일이 다른 여러 조각으로 쪼개져 있을 수 있습니다.
  • 매칭: 런들의 텍스트를 이어붙여("".join) 마커 문자열이 들어 있는지만 봅니다. 전체 문구가 아니라 식별성 높은 일부(/feedback to help us improve)로만 매칭해, 사용자가 직접 /feedback 을 친 줄 등 오탐을 피합니다.
  • 공백 치환: 매칭된 행만 각 런을 같은 길이의 공백 + 원래 스타일로 바꿉니다 → 폭 유지.
  • in-place 변형 금지(중요): render 캐시의 행 객체를 공유하므로 원본 리스트나 행을 제자리에서 고치면 캐시가 오염됩니다. 그래서 매칭이 있을 때만 out = list(rows) 로 얕은 복사본을 만들어 그 행만 교체하고, 매칭이 없으면 원본 객체를 그대로 반환합니다(핫패스 무할당).

2) 세션 종료 배너 가리기

_FEEDBACK_PROMPT_MARK = "How is Claude doing this session"
_FEEDBACK_OPT_MARKS = ("Bad", "Fine", "Good", "Dismiss")

def _blank_feedback_banner(rows):
    texts = ["".join(t for t, _ in row) for row in rows]
    if not any(_FEEDBACK_PROMPT_MARK in tx for tx in texts):
        return rows
    out = None
    for i, tx in enumerate(texts):
        if _FEEDBACK_PROMPT_MARK in tx or all(m in tx for m in _FEEDBACK_OPT_MARKS):
            if out is None:
                out = list(rows)
            out[i] = [[" " * len(t), st] for t, st in rows[i]]
    return out if out is not None else rows
  • 배너는 프롬프트 줄 + 평가옵션 줄 두 줄입니다.
  • 선(先) 게이트: 화면 어디에도 프롬프트 줄(How is Claude doing this session)이 없으면 즉시 원본 반환 → 핫패스에 영향이 없고, 평가옵션 줄만 단독으로 잘못 가리는 일도 막습니다.
  • 평가옵션 줄은 Bad·Fine·Good·Dismiss모두 들어 있을 때만 가립니다(오탐 방지). 그리고 이 검사는 프롬프트 줄이 화면에 함께 있을 때만 도달합니다.

3) 플러그인 클래스와 훅

class _ClaudeDisableFeedbackPlugin:
    name = "claude-disable-feedback"
    description = "Claude Code 피드백 문구 숨김 — 시작 팁·세션 종료 평가 배너 가림"
    category = "Claude"

    def server_filter_rows(self, server, pane, rows):
        if not (getattr(pane, "_claude", None) or getattr(pane, "_hdr_claude", None)):
            return rows
        rows = _blank_feedback_tip(rows)
        rows = _blank_feedback_banner(rows)
        return rows

PLUGIN = _ClaudeDisableFeedbackPlugin()
  • 모듈은 끝에서 PLUGIN 인스턴스 하나를 노출합니다. 레지스트리는 이 객체에서 훅 메서드를 찾습니다.
  • 패널 게이트(부드러운 참조): Claude 패널이 아니면 즉시 원본을 반환합니다. 게이트는 claude-code 플러그인이 패널에 설치하는 두 속성을 getattr 로 부드럽게 봅니다.
    • _claude — 현재 Claude 가 실행 중.
    • _hdr_claude — 디바운스된 Claude 신호(잠깐의 전이 창 동안 True 로 남음).
    • claude-code 플러그인이 없으면 두 속성이 다 없어 모든 패널이 그냥 통과(no-op)합니다. getattr(..., None) 덕분에 결합 없이 안전합니다.
  • 왜 두 속성을 OR 하나? 피드백 배너는 세션이 끝나는 순간 뜨는데, 그 시점이면 _claude 가 이미 None 으로 떨어졌을 수 있습니다. 그러면 _claude 만 보면 배너가 게이트를 통과 못 해 그대로 보입니다. _hdr_claude 는 그 전이 창에서 한동안 True 로 남아 배너가 뜨는 바로 그 순간을 덮어줍니다.

왜 비모달 오버레이엔 표시-필터 > 키-주입인가

이 플러그인이 주는 일반 교훈입니다.

키 주입 (Esc/Dismiss 자동 전송) 표시 필터 (server_filter_rows)
패널 상태 변경함 — 작동 중 턴을 interrupt 할 수 있음 건드리지 않음
타이밍 render 와 레이스 — 키가 엉뚱한 순간에 도착 가능 render 직전 결정적 변형, 레이스 없음
실패 모드 사용자 작업 중단(되돌리기 어려움) 최악이라도 "잠깐 보임"(무해)
복잡도 배너 출현 감지 + 쿨다운/디바운스 필요 행 텍스트 매칭 한 번

비모달 오버레이(닫지 않아도 작업이 막히지 않는 것)는 키를 주입하기보다 표시 필터로 가리는 쪽이 안전합니다. 키 주입은 busy 턴과 레이스가 나기 때문입니다. (반대로 모달이라 닫지 않으면 진행이 정말 막히는 경우엔 다른 접근이 필요할 수 있습니다.)

delete-to-disable

기능 전체가 pytmuxlib/plugins/claude-disable-feedback/ 한 디렉토리 안에 있습니다. 디렉토리를 통째로 지우거나 플러그인 관리 팝업에서 끄면 — 두 피드백 문구가 다시 화면에 보일 뿐, 코어와 claude-code 는 그대로 동작합니다. 코어는 이 플러그인을 직접 import 하지 않고 server_filter_rows 훅으로만 닿으므로, 플러그인이 없으면 행은 변형 없이 지나갑니다. 이것이 pytmux 플러그인의 delete-to-disable 계약입니다.

테스트 방법

이 플러그인은 순수 함수에 가까워 단위 테스트가 쉽습니다. textual/PTY 같은 무거운 인프라가 필요 없습니다.

  1. 헬퍼 함수 직접 호출_blank_feedback_tip / _blank_feedback_banner 에 가짜 rows(런 목록)를 만들어 넣고 결과를 확인합니다.

    • 매칭 행이 같은 폭의 공백으로 바뀌는지(길이 보존).
    • 비매칭 행은 원본 객체 그대로(is 동일성)인지 → in-place 미변형 + 핫패스 무할당 확인.
    • 평가옵션 줄은 프롬프트 줄이 함께 있을 때만 가려지는지(단독이면 안 가려짐).
    • /feedback 을 사용자가 직접 친 줄 같은 오탐 케이스가 살아남는지.
  2. 훅 게이트 테스트server_filter_rows_claude/_hdr_claude 둘 다 없는 가짜 pane 을 주면 원본을 그대로 돌려주는지(no-op), _hdr_claude 만 True 일 때 배너가 가려지는지(세션 종료 전이 창 회귀 방지).

  3. 전체 스위트로 권위 확인:

    python3 tests/run.py

    요약줄(N passed, 0 failed)을 직접 확인하세요. run.py 는 실패해도 종료코드가 0 일 수 있고, 서브셋만 돌리면 플러그인 믹스인 poison 으로 가짜 실패가 날 수 있어 권위는 항상 전체 스위트입니다.

최소 플러그인 체크리스트

이 플러그인을 템플릿 삼아 새 훅 플러그인을 만들 때:

  • pytmuxlib/plugins/<name>/__init__.py 한 파일로 시작.
  • 무거운 의존(textual/rich) import 금지 — 서버 프로세스도 같은 코드를 로드합니다.
  • 필요한 레지스트리 훅 메서드만 구현(여기선 server_filter_rows 하나).
  • 클래스에 name / description / category 속성, 모듈 끝에 PLUGIN = <Class>() 인스턴스 노출.
  • 다른 플러그인의 상태에 getattr(..., None) 로 부드럽게 접근 — 그 플러그인이 없어도 no-op.
  • 핫패스 가드(이 패널/이 화면이 아니면 즉시 원본 반환)와 in-place 변형 금지(매칭 있을 때만 복사본).

관련 문서

Clone this wiki locally