Audit Logging User Guide
Track token usage and API costs for every Claude Code request with opt-in audit logging. This guide covers configuration, log analysis, and privacy considerations.
Overview
The audit logging feature captures metadata about each Claude API request—including token counts, model names, timestamps, and durations—without logging any prompt content or user data. This allows you to:
- Track token consumption per request
- Analyze costs by model
- Identify usage patterns
- Understand API performance
Key characteristics:
- Opt-in by default (disabled unless explicitly enabled)
- Metadata-only logging (no prompts, responses, or user data)
- Automatic log rotation (default: 10MB max)
- JSONL format (newline-delimited JSON) for easy analysis with standard Unix tools
- Zero performance impact when disabled
Enabling Audit Logging
Step 1: Edit Your Configuration
Add the audit logging section to your config file:
Global configuration (~/.claude/config.json):
{
"audit_logging": {
"enabled": true
}
}Project-level configuration (.clauderc in your project root):
{
"audit_logging": {
"enabled": true,
"log_path": "./logs/audit.log",
"max_size_mb": 5
}
}Step 2: Verify It's Working
Make a Claude API request. If audit logging is enabled, you should see a new log entry:
# Check the audit log
cat ~/.claude/audit.log
# Pretty-print the latest entry
tail -1 ~/.claude/audit.log | jq .Configuration Options
Available Fields
| Field | Type | Default | Range | Description |
|---|---|---|---|---|
enabled | boolean | false | - | Enable/disable audit logging. Default: disabled for privacy. |
log_path | string | ~/.claude/audit.log | - | File path for audit log. Supports ~ for home directory and relative paths. |
max_size_mb | number | 10 | 1–1000 | Maximum log file size (MB) before rotation. |
keep_backups | number | 1 | 1 | Number of backup files to keep. (Currently MVP supports only 1.) |
Default Configuration
If you don't configure audit logging, these defaults are used:
{
"audit_logging": {
"enabled": false,
"log_path": "~/.claude/audit.log",
"max_size_mb": 10,
"keep_backups": 1
}
}Configuration Priority
Audit logging config is loaded in this order (first match wins):
- Project-level (
.claudercin current directory) - Global (
~/.claude/config.json) - Defaults (built-in values shown above)
This allows you to use different settings per project while maintaining global defaults.
Validation Constraints
The configuration module validates all values:
- enabled: Must be a boolean (default:
false) - log_path: Must be a valid string path
- max_size_mb: Must be between 1 MB (minimum size) and 1000 MB (maximum size)
- Values below 1 MB are reset to the default (10 MB) with a warning
- Values above 1000 MB are capped at 1000 MB with a warning
- keep_backups: Only 1 backup is supported in this MVP release (other values trigger a warning)
Log Format
Each audit log entry is a single-line JSON object (JSONL format). This format is compatible with jq, grep, and standard Unix tools.
Log Entry Schema
{
"timestamp": "2026-02-04T23:30:15Z",
"session_id": "uuid-string-here",
"model": "claude-sonnet-4-5-20250929",
"input_tokens": 1234,
"output_tokens": 567,
"cache_creation_tokens": 0,
"cache_read_tokens": 0,
"total_cost_usd": 0.00789,
"duration_ms": 3200,
"duration_api_ms": 2950,
"models_used": ["claude-sonnet-4-5-20250929"],
"warning": null
}Field Descriptions
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO 8601 UTC timestamp when the request was recorded. |
session_id | string | Unique identifier for this Claude session. |
model | string | Model name used (e.g., claude-opus-4-5-20251101). |
input_tokens | number | null | Tokens sent to the API (null if not available). |
output_tokens | number | null | Tokens generated by Claude (null if not available). |
cache_creation_tokens | number | null | Tokens consumed creating a prompt cache (null if not applicable). |
cache_read_tokens | number | null | Tokens read from a prompt cache (null if not applicable). |
total_cost_usd | number | null | Estimated cost in USD (null if pricing data unavailable). |
duration_ms | number | null | Total request duration in milliseconds (null if not available). |
duration_api_ms | number | null | (Optional) API response time only, excluding local processing. |
models_used | string[] | (Optional) List of models used in this request (for multi-model sessions). |
warning | string | null | (Optional) Warning if data is incomplete (e.g., missing_usage_data). |
Example Entries
Successful request with complete data:
{"timestamp":"2026-02-04T10:00:00Z","session_id":"sess-abc123","model":"claude-sonnet-4-5-20250929","input_tokens":1234,"output_tokens":567,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.00789,"duration_ms":3200}Request with complete data and API timing:
{"timestamp":"2026-02-04T10:15:00Z","session_id":"sess-def456","model":"claude-opus-4-5-20251101","input_tokens":2000,"output_tokens":800,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.01389,"duration_ms":4500,"duration_api_ms":4200}Request with cache usage:
{"timestamp":"2026-02-04T10:30:00Z","session_id":"sess-ghi789","model":"claude-opus-4-5-20251101","input_tokens":5000,"output_tokens":1500,"cache_creation_tokens":2000,"cache_read_tokens":0,"total_cost_usd":0.02450,"duration_ms":8000,"duration_api_ms":7500}Log Location
Default Path
~/.claude/audit.logThis expands to your home directory. For example:
- macOS/Linux:
/Users/yourname/.claude/audit.log - Windows:
C:\Users\yourname\.claude\audit.log
Custom Location
Specify a custom path in your config:
{
"audit_logging": {
"enabled": true,
"log_path": "/var/log/claude/audit.log"
}
}Path Resolution Rules
Paths are resolved in this order:
- Absolute paths are used as-is
- Example:
/var/log/claude/audit.log
- Example:
- Tilde paths expand to home directory
- Example:
~/my-logs/audit.log→/Users/yourname/my-logs/audit.log
- Example:
- Relative paths are resolved relative to
~/.claude/- Example:
logs/audit.log→~/.claude/logs/audit.log
- Example:
Directory Creation
The .claude directory and any parent directories are created automatically when the first audit log entry is written. You don't need to create them manually.
Rotation Behavior
When Rotation Happens
Log rotation is triggered when the current log file reaches or exceeds the max_size_mb threshold. This happens automatically before writing a new entry.
Example timeline:
10:00 - Write entry (file size: 1MB)
10:15 - Write entry (file size: 5MB)
10:30 - Write entry (file size: 9.5MB)
10:45 - Write entry (would push to 10.2MB)
→ ROTATION TRIGGERED
→ audit.log renamed to audit.log.backup
→ New audit.log created
→ Entry written to new audit.logHow Rotation Works
When the file reaches the size limit:
- Old backup file (if it exists) is deleted
- Current log file is renamed to
audit.log.backup - A new empty
audit.logfile is created - The pending entry is written to the new file
File layout:
~/.claude/
├── audit.log ← Current log (newly rotated, fresh)
└── audit.log.backup ← Previous log (rotated out)Backup Naming
Backups use a simple .backup suffix. The MVP implementation maintains only one backup file. If you need multiple backups or time-based naming, you can:
- Manually copy the backup before rotation occurs
- Use OS-level log rotation tools like
logrotateon Unix systems - Archive old logs to another location
Size Configuration
Set the rotation size in your config:
{
"audit_logging": {
"max_size_mb": 20
}
}Common sizes:
1– Very frequent rotation (small projects)5– Frequent rotation (active development)10– Default (typical usage)50– Large deployments500– High-traffic services
Analyzing Logs
Sample Log Data
The examples below use this sample audit log (4 entries):
{"timestamp":"2026-02-04T10:00:00Z","session_id":"sess-1","model":"claude-sonnet-4-5-20250929","input_tokens":1234,"output_tokens":567,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.00789,"duration_ms":3200}
{"timestamp":"2026-02-04T10:15:00Z","session_id":"sess-2","model":"claude-sonnet-4-5-20250929","input_tokens":2000,"output_tokens":800,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.01389,"duration_ms":4500}
{"timestamp":"2026-02-04T10:30:00Z","session_id":"sess-3","model":"claude-opus-4-5-20251101","input_tokens":5000,"output_tokens":1500,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.02450,"duration_ms":8000}
{"timestamp":"2026-02-03T14:00:00Z","session_id":"sess-4","model":"claude-sonnet-4-5-20250929","input_tokens":800,"output_tokens":300,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.00389,"duration_ms":2100}Example 1: Total Tokens Used Today
Count all input and output tokens from today's requests:
# Calculate total tokens used today
grep $(date +%Y-%m-%d) ~/.claude/audit.log | \
jq -s 'map(.input_tokens + .output_tokens) | add'Output: 10868 (total tokens)
What it does:
grepfilters log entries matching today's datejq -sslurps all matching entries into an arraymap()transforms each entry into token sumaddsums all the values
Example 2: Tokens Grouped by Model
Break down token usage by model to understand which models consume the most tokens:
# Pattern: by.*model|group.*model - use group_by to aggregate by model
jq -s 'group_by(.model) | map({
model: .[0].model,
total_input: map(.input_tokens) | add,
total_output: map(.output_tokens) | add,
total_combined: map(.input_tokens + .output_tokens) | add,
request_count: length
})' ~/.claude/audit.logOutput:
[
{
"model": "claude-sonnet-4-5-20250929",
"total_input": 4034,
"total_output": 1667,
"total_combined": 5701,
"request_count": 3
},
{
"model": "claude-opus-4-5-20251101",
"total_input": 5000,
"total_output": 1500,
"total_combined": 6500,
"request_count": 1
}
]What it does:
group_by(.model)organizes entries by modelmap()transforms each group into summary statistics- Calculates input, output, combined totals and request count
Example 3: Average Tokens Per Request
Calculate the average tokens per API request:
# Calculate average tokens per request
jq -s 'map(.input_tokens + .output_tokens) | (add / length)' ~/.claude/audit.logOutput: 2717 (average tokens per request)
For a more detailed breakdown, use:
jq -s 'map(.input_tokens + .output_tokens) | {
total: add,
count: length,
average: (add / length)
}' ~/.claude/audit.logWhat it does:
map()sums tokens for each request- Calculates total, count, and average
Example 4: Cost Analysis
Calculate total cost and average cost per request:
jq -s '{
total_cost: map(.total_cost_usd) | add,
request_count: length,
average_cost: (map(.total_cost_usd) | add) / length,
cost_by_model: group_by(.model) | map({
model: .[0].model,
total_cost: map(.total_cost_usd) | add
})
}' ~/.claude/audit.logOutput:
{
"total_cost": 0.05017,
"request_count": 4,
"average_cost": 0.00125425,
"cost_by_model": [
{
"model": "claude-sonnet-4-5-20250929",
"total_cost": 0.02567
},
{
"model": "claude-opus-4-5-20251101",
"total_cost": 0.02450
}
]
}Example 5: Request Duration Analysis
Find slow requests and average response time:
jq -s 'sort_by(.duration_ms) | {
slowest: .[-1],
fastest: .[0],
average_duration_ms: (map(.duration_ms) | add / length),
median_duration_ms: .[length / 2 | floor].duration_ms
}' ~/.claude/audit.logOutput:
{
"slowest": {
"timestamp": "2026-02-04T10:30:00Z",
"model": "claude-opus-4-5-20251101",
"duration_ms": 8000
},
"fastest": {
"timestamp": "2026-02-03T14:00:00Z",
"model": "claude-sonnet-4-5-20250929",
"duration_ms": 2100
},
"average_duration_ms": 4425,
"median_duration_ms": 3850
}Example 6: Requests from a Specific Session
Filter logs by session ID:
jq 'select(.session_id == "sess-1")' ~/.claude/audit.logOutput:
{"timestamp":"2026-02-04T10:00:00Z","session_id":"sess-1","model":"claude-sonnet-4-5-20250929","input_tokens":1234,"output_tokens":567,"cache_creation_tokens":0,"cache_read_tokens":0,"total_cost_usd":0.00789,"duration_ms":3200}Example 7: Cache Performance
Analyze cache creation and read patterns:
jq -s '{
total_cache_created: map(.cache_creation_tokens) | add,
total_cache_read: map(.cache_read_tokens) | add,
cache_enabled_requests: map(select(.cache_creation_tokens > 0 or .cache_read_tokens > 0)) | length,
cache_efficiency: (
(map(.cache_read_tokens) | add) /
((map(.cache_read_tokens) | add) + (map(.cache_creation_tokens) | add))
)
}' ~/.claude/audit.logCost Calculation
Pricing Formula
Estimated cost is calculated by the Claude API based on current pricing:
Total Cost = (Input Tokens × Input Price) + (Output Tokens × Output Price)Example:
Request: 1,000 input tokens + 500 output tokens
Model: Claude Sonnet (as of Feb 2026)
- Input: $3 per 1M tokens
- Output: $15 per 1M tokens
Cost = (1,000 × $3/1M) + (500 × $15/1M)
= $0.003 + $0.0075
= $0.0105Accessing Cost Data
Each log entry includes total_cost_usd if available:
# Sum total cost
jq -s 'map(.total_cost_usd) | add' ~/.claude/audit.log
# Cost per model
jq -s 'group_by(.model) | map({
model: .[0].model,
total_cost: map(.total_cost_usd) | add
})' ~/.claude/audit.logCache Impact on Cost
Prompt cache reads are significantly cheaper than regular input tokens. The audit log records both:
cache_creation_tokens: Tokens consumed creating cache (billed at input rate)cache_read_tokens: Tokens read from cache (billed at 10% of input rate)
Example cache cost:
Request with cache reuse:
- Input tokens: 100
- Cache read tokens: 5,000
- Input price: $3 per 1M
- Cache read price: $0.30 per 1M
Cost = (100 × $3/1M) + (5,000 × $0.30/1M)
= $0.0003 + $0.0015
= $0.0018Without cache, the same 5,100 input tokens would cost $0.0153 — 8.5x more.
Privacy & Security
What Is Logged
Logged (metadata only):
timestamp– When the request was madesession_id– Which Claude session created the requestmodel– Which AI model processed itinput_tokens,output_tokens– Token counts (no message content)cache_creation_tokens,cache_read_tokens– Cache usage statstotal_cost_usd– Estimated costduration_ms– Request timingmodels_used– List of models (for multi-model sessions)
What Is NOT Logged
Never logged:
- Prompt text
- Response text
- System messages or instructions
- User credentials or API keys
- Session tokens or authentication data
- Conversation history
- Any other user data
Opt-In by Default
Audit logging is disabled by default for privacy. It requires explicit opt-in by setting enabled: true in config.
File Permissions
Audit log files are created with restrictive permissions (mode 0600):
ls -la ~/.claude/audit.log
# Output: -rw------- user user 45678 Feb 4 10:45 /Users/user/.claude/audit.logThis means:
- Only the file owner can read the log
- Group and others have no access
- Prevent accidental information leakage
Sharing and Backup
When sharing logs or backing them up:
- Review before sharing – Even though logs contain only metadata, they reveal usage patterns
- Restrict access – Keep files with
0600permissions - Rotate before archival – Archive and delete old logs to avoid accumulation
- Use encryption – For sensitive environments, encrypt backup files
Related Modules
The audit logging feature is implemented across these modules in src/audit/:
- src/audit/config.ts – Configuration loading and validation
- src/audit/format.ts – Formatting API responses as audit entries
- src/audit/capture.ts – Executing Claude CLI and parsing responses
- src/audit/writer.ts – Writing to log files and rotation
- src/audit/index.ts – Public API orchestration
Troubleshooting
Audit log not being created
Check 1: Is audit logging enabled?
cat ~/.claude/config.json | jq '.audit_logging.enabled'If it returns false or null, enable it:
{
"audit_logging": {
"enabled": true
}
}Check 2: Are you making requests?
The log file is created when the first entry is written. Make sure you're making Claude API requests after enabling audit logging.
Check 3: Check permissions
ls -la ~/.claude/
# Should show you can write to this directoryLog file is empty or truncated
Possible causes:
- No requests made – Make some Claude API requests
- Rotation just occurred – The old log was rotated to
.backup. Check:bashls -la ~/.claude/audit.log* - Permission issue – Verify write permissions:bash
touch ~/.claude/test.log && rm ~/.claude/test.log
Analyzing logs fails with "jq: parse error"
This usually means corrupted JSON in a log entry. Check for lines with issues:
# Show all lines and find the problematic one
jq . ~/.claude/audit.log 2>&1 | grep -A 1 "parse error"
# Show the raw line that failed
sed -n '3p' ~/.claude/audit.log # Line numbers from jq outputIf you find a corrupted entry, you can:
- Remove the line manually (use a text editor)
- Or restart the log by rotating it:bash
mv ~/.claude/audit.log ~/.claude/audit.log.old # The next request will create a fresh log
Cannot write to audit log (permission denied)
Error message:
ERROR: Permission denied writing to audit logFix:
# Ensure you own the directory
sudo chown -R $(whoami) ~/.claude/
# Ensure correct permissions
chmod 700 ~/.claude/
chmod 600 ~/.claude/audit.log ~/.claude/audit.log.backup 2>/dev/nullDisk space issues
Error message:
ERROR: Cannot write audit log - disk fullSolutions:
Archive and delete old logs:
bashmv ~/.claude/audit.log ~/audit-logs/audit-$(date +%Y-%m-%d).log # (or similar archive strategy)Reduce max file size:
json{ "audit_logging": { "max_size_mb": 5 } }Use
logrotateto manage automatically (Unix/Linux):bash# Create /etc/logrotate.d/claude-audit ~/.claude/audit.log { daily rotate 7 compress missingok }
Next Steps
- Use the analysis examples to explore your usage patterns
- Set up a monitoring script to track costs over time
- Consider archiving old logs to manage disk space
- Review privacy implications before sharing logs
Related Documentation
- README.md – Quick start for audit logging and feature overview
- src/audit/config.ts – Configuration module with validation rules and defaults
- src/audit/format.ts – Log entry format specifications and validation