아카이브 / 개발

work-pulse — Claude Code 세션 활동 타임라인

유형개발
기간2026.03
역할설계·구현
구분개인 프로젝트
언어Python
주요 기술Click · Rich · pytest · uv

목차프로젝트 요약 · 담당 범위 · 문제와 구현 접근 · 이 작업에서 한 일 · 결과물과 원문 · 구현 상세

프로젝트 요약

병렬로 돌리는 여러 Claude Code 세션의 파일 작업, 프로젝트 전환, 커밋을 hook으로 기록하고 터미널에서 세션 현황·타임라인·일일 요약으로 보여주는 개인용 CLI.

담당 범위

설계·구현을 맡은 개인 프로젝트다. 공개 저장소에 코드와 사용 방법을 남겼다.

문제와 구현 접근

여러 Claude Code 세션을 동시에 돌리면서 어떤 프로젝트에 시간을 얼마나 쓰는지 파악하기 어려웠던 문제에서 출발했다.

PostToolUse hook은 Read·Write·Edit의 파일 경로로 프로젝트를 추론해 이벤트를 남기고, 같은 세션에서 프로젝트가 바뀌면 switch 이벤트를, Bash의 git commit은 commit 이벤트를 기록한다. SessionEnd hook은 세션 transcript에서 토큰 수를 읽는다. 여러 세션이 동시에 쓰기 때문에 DB 없이 날짜별 JSONL 파일에 잠금을 건 뒤 한 줄씩 추가한다.

프로젝트는 작업 루트 아래 첫 디렉터리 이름이나 가장 긴 경로 매핑으로 추론한다. status·timeline·summary·tokens 명령으로 세션 현황과 하루 작업을 터미널 표로 보여주고, cleanup으로 오래된 기록을 압축한다. 토큰 집계는 best-effort이고 비용 집계는 아직 없다.

이 작업에서 한 일

아이디어를 실제로 작동하는 코드로 만들고, 다른 사람이 설치하고 사용할 수 있도록 설명서를 함께 작성했다.

결과물과 원문

관련 등록공보·논문·저장소는 아래 링크에서 볼 수 있다.

구현 상세

README공개 저장소의 구조·기능·실행 문서

work-pulse

개인 작업 Activity Timeline (Claude Code 세션 추적)

배경

하루에 8개 이상의 Claude Code 세션을 병렬로 돌리는데, 어떤 프로젝트에 시간을 얼마나 쓰고 있는지 파악이 안 된다. 토큰/비용 모니터링과 프로젝트별 시간 분배를 한눈에 볼 수 있는 도구가 필요했다.

동작 방식

  1. Claude Code hook이 실행될 때마다 hook 모듈이 stdin으로 hook JSON을 받는다.
  2. 파일 경로에서 프로젝트를 추론해 이벤트를 날짜별 JSONL 파일에 한 줄씩 추가한다. 여러 세션이 동시에 쓰기 때문에 fcntl.flock으로 잠근 뒤 append 한다. DB는 쓰지 않는다.
  3. pulse CLI가 JSONL을 읽어 Rich 표로 세션 현황, 타임라인, 일일 요약을 보여준다.

세션은 session_id의 앞 3글자로 구분한다. 기록되는 이벤트 타입은 read / write / edit, switch(같은 세션에서 프로젝트가 바뀜), commit, session_end 이다.

기술 스택

  • 언어: Python 3.13+
  • CLI: Click
  • UI: Rich (터미널 포맷팅)
  • 설정: PyYAML
  • 테스트: pytest
  • 패키지: uv (pyproject.toml, hatchling 빌드)

fcntl을 사용하므로 macOS/Linux 환경을 전제로 한다.

설치

uv sync
uv run pulse --help

사용법

명령설명
pulse status오늘 기록된 세션별 현재 프로젝트, 마지막 동작, 경과 시간. 마지막 이벤트가 30분 넘게 지난 세션은 (idle), 종료된 세션은 (ended)로 표시
pulse timeline [--project NAME] [--last 2h]오늘 이벤트를 최신순으로 출력. --last30s/30m/2h/1d 형식
pulse summary [--date YYYY-MM-DD]하루 동안의 세션 수(active/ended)와 프로젝트별 커밋 수, 만진 파일 수
pulse tokens [--week]session_end 이벤트에 기록된 토큰 사용량. --week는 최근 7일 (아래 한계 참고)
pulse cleanup [--days 30]N일보다 오래된 JSONL 파일을 .jsonl.gz로 압축하고 원본 삭제

Hook 연결

각 hook은 모듈로 실행되며 stdin으로 JSON을 읽는다. work-pulse가 설치된 환경의 Python으로 아래 명령을 Claude Code hook 명령으로 등록해서 쓴다.

모듈연결 대상하는 일
python -m pulse.hooks.post_tool_usePostToolUseRead/Write/Edittool_input.file_path로 프로젝트를 추론해 기록하고, 세션의 직전 프로젝트와 다르면 switch 이벤트를 먼저 남긴다. Bash 명령에 git commit이 있으면 커밋 메시지(-m 또는 heredoc)를 commit 이벤트로 남긴다.
python -m pulse.hooks.session_endSessionEnd세션 transcript에서 토큰 수를 읽어(아래 한계 참고) session_end 이벤트를 남기고 세션 상태 파일을 지운다.
python -m pulse.hooks.subprocess_completed(Claude Code 기본 hook 이벤트 없음)session_id, command, exit_code를 stdin JSON으로 넘기는 별도 러너용. 성공한 git commit의 메시지와 변경 파일 수를 commit 이벤트로 남긴다.
  • session_end는 Stop이 아니라 SessionEnd에 연결한다. Stop은 응답이 끝날 때마다 실행되므로, Stop에 연결하면 매 턴 session_end 이벤트가 쌓이고 pulse status에 세션이 턴 사이마다 (ended)로 보이며, 상태 파일이 매번 지워져 턴을 넘나드는 switch 감지가 거의 동작하지 않는다.
  • Claude Code에는 subprocess_completed에 해당하는 기본 hook 이벤트가 없다. Claude Code 안에서 한 커밋은 PostToolUse(Bash)로 연결한 post_tool_use가 기록하므로, 이 모듈은 연결하지 않아도 된다.

입력 예시:

echo '{"session_id":"abc123","tool_name":"Edit","tool_input":{"file_path":"/home/me/project/demo/app.py"}}' \
  | uv run python -m pulse.hooks.post_tool_use

저장 위치

기본 디렉터리는 ~/.pulse이고, 환경 변수 PULSE_DIR로 바꿀 수 있다.

~/.pulse/
├── config.yaml              # 선택
├── events/YYYY-MM-DD.jsonl  # 날짜별 이벤트 로그
└── state/<sid>.json         # 세션별 마지막 프로젝트 (session_end 시 삭제)

이벤트 한 줄 예시:

{"sid": "abc", "type": "edit", "project": "demo", "file": "demo/app.py", "ts": "14:25:03"}

설정

~/.pulse/config.yaml이 없거나 비어 있으면 workspace_root: ~/project, 매핑 없음으로 동작한다. 파일에 없는 키도 이 기본값을 쓴다. workspace_root~는 홈 디렉터리로 펼치고 끝의 /는 뗀다.

workspace_root: /home/me/project
project_mappings:
  monorepo/code/api: monorepo/api
  monorepo/code/web: monorepo/web

프로젝트 추론 규칙:

  • workspace_root 밖의 파일은 (external)
  • project_mappings는 가장 긴 prefix가 먼저 매칭된다
  • 루트 바로 아래 파일이나 .claude 같은 점(.) 디렉터리 안의 파일은 (workspace)
  • 그 외에는 workspace_root 아래 첫 번째 디렉터리 이름

테스트

uv run pytest

현재 상태와 한계

동작하는 프로토타입이다. CLI 5개 명령과 hook 3개, core/inference/hooks/CLI 테스트가 구현되어 있다. 토큰 집계는 best-effort이고 비용 집계는 아직 없다.

  • session_end는 hook 입력의 transcript_path를 먼저 읽고, 없으면 ~/.claude/projects//<session_id>.jsonl(/sessions/<session_id>.jsonl도 확인)을 찾는다. 각 줄의 message.usage(또는 최상위 usage)에서 input_tokens/output_tokens를 메시지 id당 한 번씩 더한다. 캐시 읽기/생성 토큰은 포함하지 않으며, transcript를 찾지 못하면 토큰이 기록되지 않는다. Claude Code의 transcript 위치나 형식이 바뀌면 토큰이 기록되지 않거나 값이 맞지 않을 수 있다.
  • 비용(cost)은 기록하지 않으므로 pulse tokens의 비용은 항상 $0.00이다.
  • pulse tokens --by-project 옵션은 받기만 하고 아직 집계에 반영하지 않는다.
  • switch 이벤트는 from/to로 저장되지만 CLI는 이 값을 읽지 않아 이전 프로젝트 이름 없이 switch로만 표시된다.
  • subprocess_completed가 남기는 커밋 메시지(detail)는 타임라인에 표시되지 않는다. 표시되는 것은 post_tool_use가 남긴 커밋(message)이다.
  • 이벤트 시각은 HH:MM:SS만 저장하므로 status, timeline은 오늘 파일만 본다.

GitHub에서 최신 문서 보기 ↗