【Claude】日志审计与合规追踪配置 — 已解决

适用版本:Claude Code v1.0.x 及以上
受影响场景:企业审计、合规追踪、操作日志、安全监控、数据泄露防护
阅读时长:约 25 分钟


目录

  1. 问题现象
  2. 原理深挖:Claude Code 日志体系
  3. 根因分析:审计缺失的五大根因
  4. 多方案解决:从日志到合规
  5. 验证回归:审计配置验证
  6. 避坑最佳实践
  7. 附录:审计配置速查表

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()),

![配图](https://i-blog.csdnimg.cn/img_convert/3b093a3e2ba4b771c9fbeccb12bcbfca.png)
        "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、企业内部等合规审计要求。

核心要点回顾:

  1. Hook 审计:用 PreToolUse/PostToolUse Hook 记录所有工具调用
  2. 敏感检测:自动检测 .env、密钥、危险命令的访问
  3. 成本追踪:按项目、会话、模型维度记录 Token 消耗和成本
  4. 集中管理:用 LogCentralizer 汇总分散的日志
  5. 实时告警:高危操作即时告警(邮件/控制台)
  6. 保留策略:90 天活跃 + 1 年归档 + 自动清理
  7. 不阻塞:审计 Hook 始终 exit(0),不影响 Claude 操作
  8. 定期审查:用审计分析工具生成定期报告
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐