【Claude】日志审计与合规追踪配置 — 已解决
·
【Claude】日志审计与合规追踪配置 — 已解决
适用版本:Claude Code v1.0.x 及以上
受影响场景:企业审计、合规追踪、操作日志、安全监控、数据泄露防护
阅读时长:约 25 分钟
目录
1. 问题现象
1.1 典型问题表现
问题一:无法追踪 Claude Code 的操作历史
# Claude Code 修改了文件,但不知道什么时候改了什么
git diff
# src/config.py 被修改了,但不知道是 Claude Code 还是人工修改
# 没有操作日志
问题二:企业审计要求记录所有 AI 操作
合规要求:
- 记录所有 Claude Code 的文件读写操作
- 记录所有命令执行
- 记录所有 API 调用的 Token 消耗
- 日志保留 90 天
- 支持审计查询
问题三:无法检测敏感数据泄露
# Claude Code 可能读取了 .env 文件或密钥
# 但没有日志记录哪些敏感文件被访问
# 无法在事后检测数据泄露
问题四:成本无法按项目/用户追踪
# 多个项目使用同一个 API Key
# 月底账单 $500,但不知道哪个项目花了多少
# 缺少按项目/会话的成本追踪
问题五:CI 中的操作无审计
# CI 环境中使用 Claude Code
claude -p --dangerously-skip-permissions "修复 bug"
# 没有记录 Claude 做了什么操作
# 如果 Claude 修改了不该改的文件,无法追踪
2. 原理深挖:Claude Code 日志体系
2.1 日志层级
┌─────────────────────────────────────────────────────┐
│ Claude Code 日志层级 │
├─────────────────────────────────────────────────────┤
│ │
│ Layer 1: 会话日志 (内置) │
│ ├── ~/.claude/projects/<project>/sessions/ │
│ ├── JSONL 格式,记录每轮对话 │
│ ├── 包含: 消息内容、工具调用、Token 消耗 │
│ └── 自动生成,无需配置 │
│ │
│ Layer 2: Verbose 日志 (--verbose) │
│ ├── 实时输出到 stderr │
│ ├── 包含: API 请求/响应、工具调用详情 │
│ └── 用于调试 │
│ │
│ Layer 3: Hook 日志 (自定义) │
│ ├── PreToolUse / PostToolUse hooks │
│ ├── 记录每次工具调用的详细信息 │
│ └── 需要手动配置 │
│ │
│ Layer 4: API 使用日志 (Anthropic Console) │
│ ├── console.anthropic.com │
│ ├── 记录 API 调用次数、Token 消耗、成本 │
│ └── 按组织/API Key 维度统计 │
│ │
│ Layer 5: 系统级日志 (OS) │
│ ├── shell history (~/.zsh_history) │
│ ├── 文件系统审计 (auditd/FSEvents) │
│ └── 需要操作系统级配置 │
│ │
└─────────────────────────────────────────────────────┘
2.2 会话日志格式
// ~/.claude/projects/<project-hash>/sessions/<session-id>.jsonl
{"type":"user","message":"修复 auth.py 的 bug","timestamp":"2025-01-15T10:00:00Z"}
{"type":"assistant","message":"我来检查 auth.py...","timestamp":"2025-01-15T10:00:02Z","model":"claude-sonnet-4-20250514","usage":{"input":1500,"output":200}}
{"type":"tool_use","tool":"Read","input":{"file_path":"src/auth.py"},"timestamp":"2025-01-15T10:00:03Z"}
{"type":"tool_result","tool":"Read","output":"<file content>","timestamp":"2025-01-15T10:00:03Z"}
{"type":"tool_use","tool":"Edit","input":{"file_path":"src/auth.py","old_str":"...","new_str":"..."},"timestamp":"2025-01-15T10:00:10Z"}
{"type":"tool_result","tool":"Edit","output":"成功","timestamp":"2025-01-15T10:00:10Z"}
{"type":"assistant","message":"已修复 bug","timestamp":"2025-01-15T10:00:15Z","usage":{"input":2000,"output":150}}
2.3 Hook 审计机制
Claude Code Hook 审计流程:
Claude 要执行操作 (如 Edit)
↓
PreToolUse Hook 被触发
→ 记录: 时间、工具名、输入参数
→ 可以阻止操作 (返回 deny)
↓
操作执行
↓
PostToolUse Hook 被触发
→ 记录: 执行结果、耗时、状态
↓
审计日志写入
配置位置: .claude/settings.json → hooks
2.4 合规审计需求矩阵
| 合规标准 | 审计要求 | Claude Code 对应 |
|---|---|---|
| SOC 2 | 操作日志、访问控制 | Hook 日志 + 权限配置 |
| GDPR | 数据访问记录、删除权 | 文件读取日志 |
| HIPAA | PHI 访问审计 | 敏感文件访问 Hook |
| ISO 27001 | 安全事件记录 | 安全操作日志 |
| 企业内部 | 成本追踪、操作审计 | Token 日志 + Hook |
3. 根因分析:审计缺失的五大根因
3.1 根因一:未配置 Hook
Claude Code 默认不记录操作日志,需要通过 Hook 配置审计日志。
3.2 根因二:会话日志不集中
会话日志分散在多个项目目录中,难以集中查询和分析。
3.3 根因三:缺少成本追踪
没有按项目、用户、会话维度追踪 Token 消耗和成本。
3.4 根因四:敏感文件访问无告警
Claude Code 读取 .env、密钥等文件时没有实时告警机制。
3.5 根因五:日志保留策略缺失
会话日志和 Hook 日志没有自动清理或归档策略,可能无限增长。
4. 多方案解决:从日志到合规
4.1 方案一:Hook 审计系统
// .claude/settings.json — 审计 Hook 配置
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/audit-pre-tool.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/audit-post-tool.py"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/audit-session-end.py"
}
]
}
]
}
}
#!/usr/bin/env python3
# .claude/hooks/audit-pre-tool.py — 工具调用前审计
import json
import sys
import os
from datetime import datetime
from pathlib import Path
# 审计日志目录
AUDIT_DIR = Path(os.environ.get("CLAUDE_AUDIT_DIR", ".claude/audit"))
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
# 敏感文件模式
SENSITIVE_PATTERNS = [
".env", "id_rsa", "id_ed25519", "credentials",
"secret", "token", "apikey", "api_key",
"password", "private_key", ".pem", ".key"
]
def is_sensitive(filepath):
"""检查是否是敏感文件"""
filepath_lower = str(filepath).lower()
for pattern in SENSITIVE_PATTERNS:
if pattern in filepath_lower:
return True
return False
def log_audit(event_type, tool_name, input_data, alert=False):
"""写入审计日志"""
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"event": event_type,
"tool": tool_name,
"input": input_data,
"project": os.getcwd(),
"session": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
"user": os.environ.get("USER", "unknown"),
"alert": alert
}
# 按日期分文件
date_str = datetime.utcnow().strftime("%Y-%m-%d")
log_file = AUDIT_DIR / f"audit-{date_str}.jsonl"
with open(log_file, "a") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
# 敏感操作实时告警
if alert:
alert_file = AUDIT_DIR / "alerts.jsonl"
with open(alert_file, "a") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
print(f"⚠ AUDIT ALERT: 敏感文件访问 {input_data}", file=sys.stderr)
# 读取 Hook 输入
try:
hook_input = json.load(sys.stdin)
tool_name = hook_input.get("tool_name", "unknown")
tool_input = hook_input.get("tool_input", {})
# 检查敏感文件
alert = False
if tool_name in ["Read", "Write", "Edit"]:
filepath = tool_input.get("file_path", "")
if is_sensitive(filepath):
alert = True
# Bash 命令审计
if tool_name == "Bash":
command = tool_input.get("command", "")
# 检查危险命令
dangerous = ["rm -rf", "sudo", "curl.*|.*sh", "wget.*|.*sh"]
import re
for pattern in dangerous:
if re.search(pattern, command):
alert = True
break
log_audit("pre_tool_use", tool_name, tool_input, alert)
except Exception as e:
# 审计日志不应阻止操作
print(f"Audit error: {e}", file=sys.stderr)
# 不阻止操作 (exit 0)
sys.exit(0)
#!/usr/bin/env python3
# .claude/hooks/audit-post-tool.py — 工具调用后审计
import json
import sys
import os
from datetime import datetime
from pathlib import Path
AUDIT_DIR = Path(os.environ.get("CLAUDE_AUDIT_DIR", ".claude/audit"))
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
def log_audit(event_type, tool_name, input_data, output_data, duration=None):
"""写入工具调用后审计日志"""
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"event": event_type,
"tool": tool_name,
"input": input_data,
"output_summary": str(output_data)[:500] if output_data else None,
"duration_ms": duration,
"project": os.getcwd(),
"session": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
"status": "success"
}
date_str = datetime.utcnow().strftime("%Y-%m-%d")
log_file = AUDIT_DIR / f"audit-{date_str}.jsonl"
with open(log_file, "a") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
try:
hook_input = json.load(sys.stdin)
tool_name = hook_input.get("tool_name", "unknown")
tool_input = hook_input.get("tool_input", {})
tool_output = hook_input.get("tool_output", {})
log_audit("post_tool_use", tool_name, tool_input, tool_output)
except Exception as e:
print(f"Audit error: {e}", file=sys.stderr)
sys.exit(0)
4.2 方案二:成本追踪系统
#!/usr/bin/env python3
# .claude/hooks/cost-tracker.py — Token 成本追踪
import json
import sys
import os
from datetime import datetime
from pathlib import Path
COST_LOG_DIR = Path(".claude/audit/costs")
COST_LOG_DIR.mkdir(parents=True, exist_ok=True)
# 模型定价 (每百万 Token)
PRICING = {
"claude-opus-4-20250514": {"input": 15.0, "output": 75.0, "cache_read": 1.5, "cache_write": 18.75},
"claude-sonnet-4-20250514": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75},
"claude-haiku-4-20250422": {"input": 0.25, "output": 1.25, "cache_read": 0.025, "cache_write": 0.3125},
}
def calculate_cost(model, usage):
"""计算 API 调用成本"""
pricing = PRICING.get(model, PRICING["claude-sonnet-4-20250514"])
input_tokens = usage.get("input_tokens", 0)
output_tokens = usage.get("output_tokens", 0)
cache_read = usage.get("cache_read_input_tokens", 0)
cache_write = usage.get("cache_creation_input_tokens", 0)
cost = (
input_tokens * pricing["input"] / 1_000_000 +
output_tokens * pricing["output"] / 1_000_000 +
cache_read * pricing["cache_read"] / 1_000_000 +
cache_write * pricing["cache_write"] / 1_000_000
)
return round(cost, 6)
def log_cost(model, usage):
"""记录成本"""
cost = calculate_cost(model, usage)
entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"model": model,
"input_tokens": usage.get("input_tokens", 0),
"output_tokens": usage.get("output_tokens", 0),
"cache_read_tokens": usage.get("cache_read_input_tokens", 0),
"cache_write_tokens": usage.get("cache_creation_input_tokens", 0),
"cost_usd": cost,
"project": os.path.basename(os.getcwd()),

"session": os.environ.get("CLAUDE_SESSION_ID", "unknown")
}
date_str = datetime.utcnow().strftime("%Y-%m-%d")
log_file = COST_LOG_DIR / f"cost-{date_str}.jsonl"
with open(log_file, "a") as f:
f.write(json.dumps(entry) + "\n")
# Hook 入口
try:
hook_input = json.load(sys.stdin)
# 从 Hook 输入中提取使用信息
# PostToolUse 或 Stop hook 中可能包含 usage
if "usage" in hook_input:
model = hook_input.get("model", "claude-sonnet-4-20250514")
log_cost(model, hook_input["usage"])
except:
pass
sys.exit(0)
4.3 方案三:审计日志分析工具
#!/usr/bin/env python3
"""
审计日志分析工具
查询和分析 Claude Code 操作日志
"""
import json
import os
import sys
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict
AUDIT_DIR = Path(".claude/audit")
def load_audit_logs(days=7):
"""加载最近 N 天的审计日志"""
logs = []
for i in range(days):
date = (datetime.utcnow() - timedelta(days=i)).strftime("%Y-%m-%d")
log_file = AUDIT_DIR / f"audit-{date}.jsonl"
if log_file.exists():
with open(log_file) as f:
for line in f:
try:
logs.append(json.loads(line))
except json.JSONDecodeError:
continue
return logs
def analyze_operations(logs):
"""分析操作统计"""
tool_counts = defaultdict(int)
file_access = defaultdict(int)
alerts = []
for log in logs:
tool = log.get("tool", "unknown")
tool_counts[tool] += 1
if tool in ["Read", "Write", "Edit"]:
filepath = log.get("input", {}).get("file_path", "unknown")
file_access[filepath] += 1
if log.get("alert"):
alerts.append(log)
return tool_counts, file_access, alerts
def generate_report(days=7):
"""生成审计报告"""
logs = load_audit_logs(days)
if not logs:
print("无审计日志")
return
tool_counts, file_access, alerts = analyze_operations(logs)
print(f"=== Claude Code 审计报告 ({days} 天) ===")
print(f"日志条目: {len(logs)}")
print(f"时间范围: {logs[0]['timestamp'][:10]} ~ {logs[-1]['timestamp'][:10]}")
print(f"\n--- 工具调用统计 ---")
for tool, count in sorted(tool_counts.items(), key=lambda x: -x[1]):
print(f" {tool}: {count} 次")
print(f"\n--- 文件访问 Top 10 ---")
for filepath, count in sorted(file_access.items(), key=lambda x: -x[1])[:10]:
print(f" {count}x {filepath}")
print(f"\n--- 敏感操作告警 ({len(alerts)}) ---")
for alert in alerts[-10:]: # 最近 10 条
print(f" [{alert['timestamp']}] {alert['tool']}: {alert.get('input', {})}")
# 成本分析
cost_logs = load_cost_logs(days)
if cost_logs:
total_cost = sum(l.get("cost_usd", 0) for l in cost_logs)
total_input = sum(l.get("input_tokens", 0) for l in cost_logs)
total_output = sum(l.get("output_tokens", 0) for l in cost_logs)
print(f"\n--- 成本统计 ---")
print(f" 总成本: ${total_cost:.4f}")
print(f" 输入 Token: {total_input:,}")
print(f" 输出 Token: {total_output:,}")
print(f" API 调用: {len(cost_logs)} 次")
def load_cost_logs(days=7):
"""加载成本日志"""
logs = []
cost_dir = AUDIT_DIR / "costs"
for i in range(days):
date = (datetime.utcnow() - timedelta(days=i)).strftime("%Y-%m-%d")
log_file = cost_dir / f"cost-{date}.jsonl"
if log_file.exists():
with open(log_file) as f:
for line in f:
try:
logs.append(json.loads(line))
except:
continue
return logs
def query_sensitive_access():
"""查询敏感文件访问记录"""
logs = load_audit_logs(30)
sensitive = [l for l in logs if l.get("alert")]
print(f"\n=== 敏感文件访问记录 (30 天) ===")
print(f"总计: {len(sensitive)} 次")
for log in sensitive:
timestamp = log.get("timestamp", "")
tool = log.get("tool", "")
user = log.get("user", "")
input_data = log.get("input", {})
if tool in ["Read", "Write", "Edit"]:
filepath = input_data.get("file_path", "")
print(f" [{timestamp}] {user} {tool} {filepath}")
elif tool == "Bash":
command = input_data.get("command", "")
print(f" [{timestamp}] {user} Bash: {command[:100]}")
# 使用
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "sensitive":
query_sensitive_access()
else:
generate_report(days=int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 7)
4.4 方案四:日志集中化
#!/usr/bin/env python3
"""
日志集中化:将分散的会话日志和审计日志汇总
"""
import json
import os
import shutil
from pathlib import Path
from datetime import datetime
class LogCentralizer:
"""日志集中管理器"""
def __init__(self, central_dir=None):
self.central_dir = Path(central_dir or os.environ.get(
"CLAUDE_LOG_CENTER",
os.path.expanduser("~/.claude/audit-central")
))
self.central_dir.mkdir(parents=True, exist_ok=True)
def collect_session_logs(self):
"""收集所有项目的会话日志"""
projects_dir = Path.home() / ".claude" / "projects"
if not projects_dir.exists():
return
collected = 0
for project_dir in projects_dir.iterdir():
if not project_dir.is_dir():
continue
project_name = project_dir.name
sessions_dir = project_dir / "sessions"
if not sessions_dir.exists():
continue
# 目标目录
dest_dir = self.central_dir / "sessions" / project_name
dest_dir.mkdir(parents=True, exist_ok=True)
# 复制会话日志
for session_file in sessions_dir.glob("*.jsonl"):
dest_file = dest_dir / session_file.name
# 不覆盖已收集的
if not dest_file.exists():
shutil.copy2(session_file, dest_file)
collected += 1
print(f"✓ 收集了 {collected} 个会话日志")
def collect_audit_logs(self):
"""收集项目级审计日志"""
# 遍历所有项目目录
for audit_dir in Path(".").glob("*/.claude/audit"):
project_name = audit_dir.parent.parent.name
dest_dir = self.central_dir / "audit" / project_name
dest_dir.mkdir(parents=True, exist_ok=True)
for log_file in audit_dir.glob("*.jsonl"):
dest_file = dest_dir / log_file.name
if not dest_file.exists():
shutil.copy2(log_file, dest_file)
def cleanup_old_logs(self, retention_days=90):
"""清理过期日志"""
cutoff = datetime.utcnow().timestamp() - (retention_days * 86400)
removed = 0
for log_file in self.central_dir.rglob("*.jsonl"):
if log_file.stat().st_mtime < cutoff:
log_file.unlink()
removed += 1
print(f"✓ 清理了 {removed} 个过期日志 (>{retention_days} 天)")
# 使用
centralizer = LogCentralizer()
centralizer.collect_session_logs()
centralizer.collect_audit_logs()
centralizer.cleanup_old_logs(retention_days=90)
4.5 方案五:实时告警系统
#!/usr/bin/env python3
# .claude/hooks/alert-system.py — 实时安全告警
import json
import sys
import os
import smtplib
from email.mime.text import MIMEText
from datetime import datetime
# 告警规则
ALERT_RULES = {
# 敏感文件读取
"sensitive_read": {
"patterns": [".env", "id_rsa", "credentials", "secret", "apikey"],
"severity": "HIGH",
"message": "敏感文件被读取"
},
# 危险命令
"dangerous_command": {
"patterns": ["rm -rf", "sudo ", "chmod 777", "curl.*|.*sh"],
"severity": "CRITICAL",
"message": "执行危险命令"
},
# 大量文件操作
"mass_operation": {
"threshold": 50, # 单次会话操作超过 50 次
"severity": "MEDIUM",
"message": "大量文件操作"
},
# 网络请求
"network_access": {
"patterns": ["curl", "wget", "WebFetch"],
"severity": "MEDIUM",
"message": "网络访问"
}
}
def send_alert(severity, message, details):
"""发送告警"""
timestamp = datetime.utcnow().isoformat()
# 控制台输出
print(f"[{severity}] {timestamp} - {message}", file=sys.stderr)
print(f" 详情: {json.dumps(details, ensure_ascii=False)[:200]}", file=sys.stderr)
# 写入告警文件
alert_file = Path(".claude/audit/alerts-realtime.jsonl")
alert_file.parent.mkdir(parents=True, exist_ok=True)
with open(alert_file, "a") as f:
f.write(json.dumps({
"timestamp": timestamp,
"severity": severity,
"message": message,
"details": details
}, ensure_ascii=False) + "\n")
# 高严重度发邮件 (可选)
if severity == "CRITICAL" and os.environ.get("ALERT_EMAIL"):
try:
send_email(severity, message, details)
except:
pass # 告警不应阻止操作
def send_email(severity, message, details):
"""发送邮件告警"""
smtp_host = os.environ.get("SMTP_HOST", "localhost")
smtp_port = int(os.environ.get("SMTP_PORT", 25))
from_addr = os.environ.get("ALERT_FROM", "claude-audit@company.com")
to_addr = os.environ.get("ALERT_EMAIL")
subject = f"[Claude Code {severity}] {message}"
body = f"""
时间: {datetime.utcnow().isoformat()}
严重度: {severity}
消息: {message}
详情: {json.dumps(details, ensure_ascii=False, indent=2)}
项目: {os.getcwd()}
用户: {os.environ.get('USER', 'unknown')}
"""
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = to_addr
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.sendmail(from_addr, [to_addr], msg.as_string())
# Hook 入口
try:
hook_input = json.load(sys.stdin)
tool_name = hook_input.get("tool_name", "")
tool_input = hook_input.get("tool_input", {})
# 检查敏感文件
if tool_name in ["Read", "Write", "Edit"]:
filepath = str(tool_input.get("file_path", "")).lower()
for pattern in ALERT_RULES["sensitive_read"]["patterns"]:
if pattern in filepath:
send_alert(
ALERT_RULES["sensitive_read"]["severity"],
ALERT_RULES["sensitive_read"]["message"],
{"tool": tool_name, "file": tool_input.get("file_path")}
)
break
# 检查危险命令
if tool_name == "Bash":
command = tool_input.get("command", "")
import re
for pattern in ALERT_RULES["dangerous_command"]["patterns"]:
if re.search(pattern, command):
send_alert(
ALERT_RULES["dangerous_command"]["severity"],
ALERT_RULES["dangerous_command"]["message"],
{"command": command}
)
break
# 检查网络访问
for pattern in ALERT_RULES["network_access"]["patterns"]:
if pattern in command:
send_alert(
ALERT_RULES["network_access"]["severity"],
ALERT_RULES["network_access"]["message"],
{"command": command}
)
break
except Exception as e:
print(f"Alert error: {e}", file=sys.stderr)
sys.exit(0)
4.6 方案六:日志保留策略
#!/bin/bash
# log-retention.sh — 日志保留和归档策略
# 配置
RETENTION_DAYS=${CLAUDE_LOG_RETENTION:-90}
ARCHIVE_DIR=${CLAUDE_LOG_ARCHIVE:-".claude/audit/archive"}
AUDIT_DIR=".claude/audit"
echo "=== 日志保留策略 ==="
echo "保留: ${RETENTION_DAYS} 天"
echo "归档目录: ${ARCHIVE_DIR}"
# 创建归档目录
mkdir -p "$ARCHIVE_DIR"
# 归档超过保留期的日志
CURRENT_DATE=$(date +%Y%m%d)
CUTOFF_DATE=$(date -v-${RETENTION_DAYS}d +%Y-%m-%d 2>/dev/null || date -d "-${RETENTION_DAYS} days" +%Y-%m-%d)
echo "归档 ${CUTOFF_DATE} 之前的日志..."
ARCHIVED=0
for log_file in "$AUDIT_DIR"/audit-*.jsonl; do
[ -f "$log_file" ] || continue
# 从文件名提取日期
FILE_DATE=$(basename "$log_file" | sed 's/audit-\(.*\)\.jsonl/\1/')
if [[ "$FILE_DATE" < "$CUTOFF_DATE" ]]; then
# 压缩并移到归档
gzip "$log_file"
mv "${log_file}.gz" "$ARCHIVE_DIR/"
ARCHIVED=$((ARCHIVED + 1))
fi
done
echo "✓ 归档了 ${ARCHIVED} 个日志文件"
# 清理超过 1 年的归档
echo ""
echo "清理超过 1 年的归档..."
YEAR_AGO=$(date -v-365d +%Y-%m-%d 2>/dev/null || date -d "-365 days" +%Y-%m-%d)
PURGED=0
for archive_file in "$ARCHIVE_DIR"/*.gz; do
[ -f "$archive_file" ] || continue
FILE_DATE=$(basename "$archive_file" | sed 's/audit-\(.*\)\.jsonl\.gz/\1/')
if [[ "$FILE_DATE" < "$YEAR_AGO" ]]; then
rm "$archive_file"
PURGED=$((PURGED + 1))
fi
done
echo "✓ 清理了 ${PURGED} 个过期归档"
# 日志大小报告
echo ""
echo "=== 日志大小 ==="
du -sh "$AUDIT_DIR" 2>/dev/null
du -sh "$ARCHIVE_DIR" 2>/dev/null
echo ""
echo "文件数量:"
find "$AUDIT_DIR" -name "*.jsonl" 2>/dev/null | wc -l | xargs echo " 活跃日志:"
find "$ARCHIVE_DIR" -name "*.gz" 2>/dev/null | wc -l | xargs echo " 归档日志:"
5. 验证回归:审计配置验证
5.1 审计验证脚本
#!/bin/bash
# verify-audit.sh — 验证审计配置
echo "=== 审计配置验证 ==="
# 1. 检查 Hook 配置
echo "Hook 配置:"
if [ -f ".claude/settings.json" ]; then
python3 -c "
import json
with open('.claude/settings.json') as f:
data = json.load(f)
hooks = data.get('hooks', {})
for event, hook_list in hooks.items():
for h in hook_list:
for hook in h.get('hooks', []):
print(f' ✓ {event}: {hook.get(\"command\", \"\")}')
if not hooks:
print(' ✗ 未配置审计 Hook')
" 2>/dev/null
fi
# 2. 检查审计日志目录
echo ""
echo "审计日志:"
if [ -d ".claude/audit" ]; then
LOG_COUNT=$(find .claude/audit -name "*.jsonl" | wc -l)
LOG_SIZE=$(du -sh .claude/audit 2>/dev/null | cut -f1)
echo " ✓ 目录存在: $LOG_COUNT 个日志, $LOG_SIZE"
else
echo " ✗ 审计目录不存在"
fi
# 3. 检查告警文件
echo ""
echo "告警记录:"
if [ -f ".claude/audit/alerts.jsonl" ]; then
ALERT_COUNT=$(wc -l < .claude/audit/alerts.jsonl)
echo " ✓ $ALERT_COUNT 条告警"
else
echo " - 无告警记录"
fi
# 4. 检查成本日志
echo ""
echo "成本日志:"
if [ -d ".claude/audit/costs" ]; then
COST_FILES=$(find .claude/audit/costs -name "*.jsonl" | wc -l)
echo " ✓ $COST_FILES 个成本日志文件"
else
echo " ✗ 无成本日志"
fi
echo ""
echo "=== 验证完成 ==="
5.2 验证清单
| # | 验证项 | 预期 | 方法 |
|---|---|---|---|
| 1 | Hook 配置 | PreToolUse/PostToolUse | settings.json 检查 |
| 2 | 审计日志生成 | 有日志文件 | 执行操作后检查 |
| 3 | 敏感文件告警 | 触发告警 | 读取 .env 测试 |
| 4 | 成本追踪 | 有成本记录 | 检查 costs/ 目录 |
| 5 | 日志格式 | 合法 JSONL | json.load 验证 |
| 6 | 日志保留 | 自动归档 | retention 脚本 |
| 7 | 集中化 | 日志汇总 | centralizer 工具 |
| 8 | 查询功能 | 可查询 | audit-analyzer |
6. 避坑最佳实践
6.1 审计配置原则
原则 1: Hook 审计 — 用 PreToolUse/PostToolUse 记录所有操作
原则 2: 敏感检测 — 自动检测 .env/密钥/危险命令
原则 3: 成本追踪 — 按项目/会话记录 Token 消耗
原则 4: 集中管理 — 日志汇总到统一目录
原则 5: 保留策略 — 90 天活跃 + 1 年归档
原则 6: 实时告警 — 高危操作即时通知
原则 7: 不阻塞 — 审计日志不应阻止操作
原则 8: 定期审查 — 定期分析审计报告
6.2 常见陷阱
| # | 陷阱 | 后果 | 解决 |
|---|---|---|---|
| 1 | 无 Hook | 无操作日志 | 配置审计 Hook |
| 2 | 日志不集中 | 难以查询 | 用 centralizer |
| 3 | 无成本追踪 | 不知花费 | cost-tracker Hook |
| 4 | 无敏感检测 | 数据泄露 | 敏感文件模式匹配 |
| 5 | 日志无限增长 | 磁盘满 | 保留策略 |
| 6 | 审计阻塞操作 | Claude 卡住 | Hook exit(0) |
| 7 | 无告警 | 不知风险 | 实时告警系统 |
| 8 | CI 无审计 | 操作不可追 | CI 中也配 Hook |
7. 附录:审计配置速查表
7.1 Hook 事件
| 事件 | 触发时机 | 用途 |
|---|---|---|
| PreToolUse | 工具调用前 | 记录操作、安全检查 |
| PostToolUse | 工具调用后 | 记录结果、成本统计 |
| Stop | 会话结束 | 会话总结、成本汇总 |
| Notification | 通知事件 | 实时告警 |
7.2 审计日志字段
| 字段 | 说明 | 示例 |
|---|---|---|
| timestamp | 时间戳 | 2025-01-15T10:00:00Z |
| event | 事件类型 | pre_tool_use |
| tool | 工具名 | Read/Write/Edit/Bash |
| input | 输入参数 | {"file_path": "src/app.py"} |
| project | 项目路径 | /home/user/myproject |
| session | 会话 ID | abc-123-def |
| user | 用户 | zhubo |
| alert | 是否告警 | true/false |
7.3 保留策略推荐
| 日志类型 | 活跃保留 | 归档保留 | 格式 |
|---|---|---|---|
| 操作日志 | 90 天 | 1 年 | JSONL |
| 成本日志 | 90 天 | 2 年 | JSONL |
| 告警日志 | 90 天 | 2 年 | JSONL |
| 会话日志 | 30 天 | 6 月 | JSONL |
结语
日志审计与合规追踪是企业级 Claude Code 使用的必备配置。通过 Hook 审计系统、成本追踪、敏感文件检测、实时告警、日志集中化和保留策略,可以满足 SOC 2、GDPR、企业内部等合规审计要求。
核心要点回顾:
- Hook 审计:用 PreToolUse/PostToolUse Hook 记录所有工具调用
- 敏感检测:自动检测
.env、密钥、危险命令的访问 - 成本追踪:按项目、会话、模型维度记录 Token 消耗和成本
- 集中管理:用 LogCentralizer 汇总分散的日志
- 实时告警:高危操作即时告警(邮件/控制台)
- 保留策略:90 天活跃 + 1 年归档 + 自动清理
- 不阻塞:审计 Hook 始终 exit(0),不影响 Claude 操作
- 定期审查:用审计分析工具生成定期报告
更多推荐



所有评论(0)