Skip to content

Commit b3c4e94

Browse files
solzipclaude
andcommitted
fix: 99점 달성 — 보안/호환성/UX/배포 전면 개선
보안: - checksum 범위 확장 (5파일 → 전체 .py) - double-masking guard (이미 마스킹된 텍스트 재마스킹 방지) - additional_secret_patterns config 지원 (팀 커스텀 패턴) - config --set 값 검증 (lang, timezone_offset 범위 체크) 호환성: - CI 매트릭스: Python 3.8/3.9/3.10/3.11/3.12 × 3 OS - dashboard os.chdir() 제거 → functools.partial 사용 - pyproject.toml에 pytest 설정 추가 배포: - MANIFEST.in 추가 (sdist에 LICENSE/CHANGELOG 포함) - .gitignore에 dist/build/egg-info 추가 UX: - --json 출력 플래그 (search, filter) - 동적 터미널 폭 (shutil.get_terminal_size) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent db0edf2 commit b3c4e94

File tree

9 files changed

+87
-31
lines changed

9 files changed

+87
-31
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
15-
python-version: ["3.7", "3.9", "3.11", "3.12"]
15+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
1616
fail-fast: false
1717

1818
steps:
@@ -35,5 +35,6 @@ jobs:
3535
3636
- name: Check coverage
3737
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
38+
continue-on-error: false
3839
run: |
3940
pytest tests/ --cov=claude_diary --cov-fail-under=70

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
# Python
77
__pycache__/
88
*.pyc
9+
*.egg-info/
10+
dist/
11+
build/
12+
.pytest_cache/
913

1014
# bkit plugin state
1115
.bkit/

MANIFEST.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include LICENSE
2+
include CHANGELOG.md
3+
include SECURITY.md
4+
include README.md
5+
include README.en.md

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,7 @@ Repository = "https://github.com/solzip/claude-code-hooks-diary"
4141

4242
[tool.setuptools.packages.find]
4343
where = ["src"]
44+
45+
[tool.pytest.ini_options]
46+
testpaths = ["tests"]
47+
pythonpath = ["src"]

src/claude_diary/cli/__init__.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ def main():
3333
p_search.add_argument("--category", "-c", help="Filter by category")
3434
p_search.add_argument("--from", dest="date_from", help="Start date (YYYY-MM-DD)")
3535
p_search.add_argument("--to", dest="date_to", help="End date (YYYY-MM-DD)")
36+
p_search.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
3637

3738
# filter
3839
p_filter = sub.add_parser("filter", help="Filter diary entries")
3940
p_filter.add_argument("--project", "-p", help="Filter by project")
4041
p_filter.add_argument("--category", "-c", help="Filter by category")
4142
p_filter.add_argument("--month", "-m", help="Filter by month (YYYY-MM)")
43+
p_filter.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
4244

4345
# trace
4446
p_trace = sub.add_parser("trace", help="Trace file change history")
@@ -178,6 +180,10 @@ def cmd_search(args):
178180
print("No results found for '%s'" % args.keyword)
179181
return
180182

183+
if getattr(args, 'json_output', False):
184+
print(json.dumps(results, ensure_ascii=False, indent=2))
185+
return
186+
181187
print(L("cli_found_entries") % len(results))
182188
print()
183189
for e in results:
@@ -364,15 +370,24 @@ def cmd_stats(args):
364370
_print_box_bottom()
365371

366372

373+
def _get_terminal_width():
374+
"""Get terminal width, defaulting to 52."""
375+
try:
376+
import shutil
377+
return min(max(shutil.get_terminal_size().columns - 2, 40), 100)
378+
except Exception:
379+
return 52
380+
381+
367382
def _print_box_top(title):
368-
width = 52
383+
width = _get_terminal_width()
369384
print("╔" + "═" * width + "╗")
370385
print("║ 📊 %-*s║" % (width - 4, title))
371386
print("╠" + "═" * width + "╣")
372387

373388

374389
def _print_box_bottom():
375-
print("╚" + "═" * 52 + "╝")
390+
print("╚" + "═" * _get_terminal_width() + "╝")
376391

377392

378393
# ── Weekly ──
@@ -483,12 +498,25 @@ def cmd_config(args):
483498
key, _, value = args.set_value.partition("=")
484499
key = key.strip()
485500
value = value.strip()
486-
if key in ("lang", "diary_dir"):
501+
if key == "lang":
502+
if value not in ("ko", "en"):
503+
print("Invalid lang: %s (use 'ko' or 'en')" % value)
504+
return
505+
config[key] = value
506+
elif key == "diary_dir":
487507
config[key] = value
488508
elif key == "timezone_offset":
489-
config[key] = int(value)
509+
try:
510+
tz = int(value)
511+
if not (-12 <= tz <= 14):
512+
print("Invalid timezone_offset: %s (range: -12 to 14)" % value)
513+
return
514+
config[key] = tz
515+
except ValueError:
516+
print("Invalid timezone_offset: %s (must be integer)" % value)
517+
return
490518
else:
491-
print("Unknown config key: %s" % key)
519+
print("Unknown config key: %s (available: lang, diary_dir, timezone_offset)" % key)
492520
return
493521
save_config(config)
494522
print("Set %s = %s" % (key, value))

src/claude_diary/core.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ def process_session(session_id, transcript_path, cwd):
103103

104104
# 5. Secret scan (always runs)
105105
try:
106-
scan_entry_data(entry_data)
106+
additional = config.get("security", {}).get("additional_secret_patterns", [])
107+
scan_entry_data(entry_data, additional or None)
107108
except Exception:
108109
pass
109110

src/claude_diary/dashboard.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ def serve_dashboard(diary_dir=None, port=8787):
102102
if not os.path.exists(os.path.join(dashboard_dir, "index.html")):
103103
generate_dashboard(diary_dir)
104104

105-
os.chdir(dashboard_dir)
106-
server = HTTPServer(("localhost", port), SimpleHTTPRequestHandler)
105+
import functools
106+
handler = functools.partial(SimpleHTTPRequestHandler, directory=dashboard_dir)
107+
server = HTTPServer(("localhost", port), handler)
107108
print("Dashboard: http://localhost:%d" % port)
108109
print("Press Ctrl+C to stop.")
109110

src/claude_diary/lib/audit.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,17 @@ def _compute_source_checksum():
111111
"""Compute SHA-256 hash of core source files for tamper detection."""
112112
hasher = hashlib.sha256()
113113

114-
# Hash key source files
114+
# Hash all .py source files for comprehensive tamper detection
115115
src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
116-
key_files = [
117-
"core.py", "hook.py", "config.py",
118-
os.path.join("lib", "parser.py"),
119-
os.path.join("lib", "secret_scanner.py"),
120-
]
121-
122-
for rel_path in key_files:
123-
full_path = os.path.join(src_dir, rel_path)
124-
if os.path.exists(full_path):
125-
try:
126-
with open(full_path, "rb") as f:
127-
hasher.update(f.read())
128-
except Exception:
129-
pass
116+
117+
for root, dirs, files in os.walk(src_dir):
118+
for fname in sorted(files):
119+
if fname.endswith(".py"):
120+
full_path = os.path.join(root, fname)
121+
try:
122+
with open(full_path, "rb") as f:
123+
hasher.update(f.read())
124+
except Exception:
125+
pass
130126

131127
return "sha256:" + hasher.hexdigest()[:16]

src/claude_diary/lib/secret_scanner.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,65 @@
1515
]
1616

1717

18-
def scan_and_mask(text):
18+
def scan_and_mask(text, additional_patterns=None):
1919
"""Scan text for secret patterns and mask them.
2020
21+
Args:
22+
text: Text to scan
23+
additional_patterns: Optional list of extra regex patterns to mask
24+
2125
Returns:
2226
(masked_text, mask_count)
2327
"""
2428
if not text:
2529
return text, 0
2630

31+
# Skip already-masked text
32+
if text == "****" or text.count("****") > 2:
33+
return text, 0
34+
2735
count = 0
28-
for pattern, replacement in BASIC_PATTERNS:
36+
all_patterns = list(BASIC_PATTERNS)
37+
if additional_patterns:
38+
for p in additional_patterns:
39+
all_patterns.append((p, "****"))
40+
41+
for pattern, replacement in all_patterns:
2942
new_text, n = re.subn(pattern, replacement, text)
3043
count += n
3144
text = new_text
3245

3346
return text, count
3447

3548

36-
def scan_entry_data(entry_data):
49+
def scan_entry_data(entry_data, additional_patterns=None):
3750
"""Scan and mask secrets in all text fields of entry_data.
3851
39-
Modifies entry_data in-place.
52+
Args:
53+
entry_data: Entry data dict (modified in-place)
54+
additional_patterns: Optional list of extra regex patterns
55+
4056
Returns total number of secrets masked.
4157
"""
4258
total = 0
4359

4460
# Scan user_prompts
4561
for i, prompt in enumerate(entry_data.get("user_prompts", [])):
46-
masked, count = scan_and_mask(prompt)
62+
masked, count = scan_and_mask(prompt, additional_patterns)
4763
if count > 0:
4864
entry_data["user_prompts"][i] = masked
4965
total += count
5066

5167
# Scan summary_hints
5268
for i, hint in enumerate(entry_data.get("summary_hints", [])):
53-
masked, count = scan_and_mask(hint)
69+
masked, count = scan_and_mask(hint, additional_patterns)
5470
if count > 0:
5571
entry_data["summary_hints"][i] = masked
5672
total += count
5773

5874
# Scan commands
5975
for i, cmd in enumerate(entry_data.get("commands_run", [])):
60-
masked, count = scan_and_mask(cmd)
76+
masked, count = scan_and_mask(cmd, additional_patterns)
6177
if count > 0:
6278
entry_data["commands_run"][i] = masked
6379
total += count

0 commit comments

Comments
 (0)