Security Logging skill for Claude Code

Audit trails that answer who did what, and page on it. Claude gives the app a typed security event vocabulary with actor, target, outcome, and correlation IDs, emitted through one redacting function. It routes the audit trail to storage the app cannot rewrite, sets retention per stream, and writes detections for credential stuffing, privilege grants, IDOR probing, and mass export, then proves redaction with canary secrets.

  • namesecurity-logging
  • size233 lines · ≈2,850 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Designs security event logging and detection for applications - a fixed event vocabulary for authentication, authorization, admin, and data-access events, structured JSON schema with actor, target, outcome, and correlation IDs, redaction of secrets and personal data, append-only audit trails with tamper evidence, retention, and alert rules for credential stuffing, privilege changes, and mass export. Use when adding audit logs, preparing for SOC 2 or an incident, logs lack who-did-what, secrets leak into logs, or the team needs detections that page someone.

Try asking

  • Add an audit log for logins, role changes, and data exports in our Django app
  • Check whether we are leaking tokens or passwords into our logs and fix it
  • Write alert rules that would catch credential stuffing and mass data export

Use it when

  • Adding audit logging or an activity history feature
  • Preparing for SOC 2, ISO 27001, or a customer security questionnaire
  • An incident showed the logs could not answer who did what
  • Secrets or personal data may be leaking into logs or error trackers

Skip it when

  • You are digging through existing logs to debug an outage - use log-analysis instead
  • You need distributed tracing and metrics rather than security events - use opentelemetry instead

How to check the result

Tests assert each instrumented action emits one well-formed event, canary passwords and tokens never appear in logs or the error tracker, the app role cannot delete audit objects, and each detection fires in staging.

The common mistake it prevents

Logging whole request headers or URLs on auth routes, which writes bearer tokens, cookies, and reset links straight into a log store many people can read.

~/.claude/skills/security-logging/SKILL.md

--- name: security-logging description: Designs security event logging and detection for applications - a fixed event vocabulary for authentication, authorization, admin, and… license: MIT ---

Done well means: for any security-relevant action you can answer who, what, on which object, from where, when, and with what outcome from one query; logs contain no secrets or raw credentials; the audit trail cannot be silently edited by the people it records; and at least a handful of detections page a human when they fire. Logs nobody alerts on are forensics at best.

Ground rules

  • Security events are a product feature with a schema, not console.log side effects. Emit them through one function with typed event names.
  • Log the decision, not the payload: auth.login.failure reason=bad_password, never the password, token, session ID, full card number, or reset link.
  • Failures and denials matter more than successes. Every deny from the authorization layer is an event.
  • Use UTC timestamps in RFC 3339 with milliseconds, from NTP-synced clocks.
  • The audit trail goes somewhere the application’s own credentials cannot delete or rewrite.
  • Personal data in logs is still personal data: minimize, pseudonymize where possible, and apply retention.

Workflow

- [ ] 1. Inventory what is logged today and where it goes
- [ ] 2. Define the event vocabulary and schema
- [ ] 3. Implement one emitter with redaction built in
- [ ] 4. Instrument the event sources (auth, authz, admin, data export, config)
- [ ] 5. Ship to storage with tamper resistance and retention
- [ ] 6. Write detections with thresholds and owners
- [ ] 7. Test emission, redaction, and alerts; run a tabletop query

1. Inventory

# Where logging happens and what might leak
grep -rnE "log(ger)?\.(info|warn|error|debug)|console\.(log|error)|print\(" --include=*.{py,ts,js,go,java,rb} . | wc -l
grep -rniE "log.*(password|passwd|secret|token|authorization|cookie|api[_-]?key|ssn|card)" --include=*.{py,ts,js,go,java,rb} .
# Auth and authz code paths that should emit events
grep -rnlE "login|logout|authenticate|reset_password|mfa|permission|forbidden|403" --include=*.{py,ts,js,go,java,rb} . | head -50

Record the sink (stdout to a collector, CloudWatch, Datadog, Loki, SIEM), retention, and who can delete logs.

2. Event vocabulary and schema

Use dotted names: <domain>.<object>.<verb>[.<outcome>]. The minimum set every app needs:

EventWhenSeverity hint
auth.login.success / auth.login.failureEvery login attempt, with reason codefailure: info, bursts: alert
auth.mfa.challenge.failureWrong or expired second factorwarn
auth.session.revoked / auth.logoutLogout, forced sign-outinfo
auth.password.reset.requested / .completedReset flowinfo
auth.credential.added / .removedPassword change, passkey or MFA device changeswarn
authz.deniedPolicy returned denyinfo; bursts: alert
admin.role.granted / .revokedAny privilege changewarn, always reviewed
admin.impersonation.started / .endedSupport acting as a userwarn
data.export.createdBulk export, report download, API key with read-allwarn
apikey.created / .revokedToken lifecyclewarn
config.security.changedSSO settings, IP allowlist, retention, MFA policywarn
input.validation.rejectedRepeated malformed or blocked input (aggregated)info

For the full catalog with required fields per event and reason codes, read references/event-catalog.md when instrumenting more than the core set or mapping to OCSF or ECS.

Schema (one JSON object per line):

{
  "ts": "2026-03-04T10:21:07.123Z",
  "event": "authz.denied",
  "outcome": "failure",
  "reason": "policy_deny",
  "actor": { "type": "user", "id": "usr_8f2c", "org_id": "org_19", "session_id_hash": "b1e4..." },
  "target": { "type": "invoice", "id": "inv_1043", "org_id": "org_77" },
  "action": "invoice:read",
  "source": { "ip": "203.0.113.9", "user_agent": "Mozilla/5.0 ...", "geo": "DE" },
  "request_id": "req_01HV...", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "service": "billing-api", "env": "prod", "schema_version": 1
}

Rules: actor and target always typed; IDs are internal stable IDs, not emails; session identifiers are hashed; outcome is one of success | failure | unknown.

3. One emitter with redaction

Python (structlog):

import hashlib, structlog

SENSITIVE_KEYS = {"password", "token", "secret", "authorization", "cookie", "api_key", "otp", "card_number"}

def _redact(_, __, event_dict):
    def scrub(obj):
        if isinstance(obj, dict):
            return {k: ("[REDACTED]" if k.lower() in SENSITIVE_KEYS else scrub(v)) for k, v in obj.items()}
        if isinstance(obj, list):
            return [scrub(v) for v in obj]
        return obj
    return scrub(event_dict)

structlog.configure(processors=[
    structlog.processors.TimeStamper(fmt="iso", utc=True, key="ts"),
    _redact,
    structlog.processors.JSONRenderer(),
])
_log = structlog.get_logger("security")

def security_event(event: str, *, outcome: str, actor: dict, target: dict | None = None, **fields):
    actor = dict(actor)                      # never mutate the caller's dict
    sid = actor.pop("session_id", None)
    if sid:
        actor["session_id_hash"] = hashlib.sha256(sid.encode()).hexdigest()[:16]
    # structlog stores the first positional argument under the "event" key
    _log.info(event, outcome=outcome, actor=actor, target=target, schema_version=1, **fields)

Node (pino) uses built-in path redaction:

import pino from "pino";
export const secLog = pino({
  base: { service: process.env.SERVICE_NAME, env: process.env.NODE_ENV },
  timestamp: pino.stdTimeFunctions.isoTime,
  redact: { paths: ["*.password", "*.token", "*.authorization", "req.headers.cookie", "req.headers.authorization"], censor: "[REDACTED]" },
}).child({ channel: "security" });

secLog.info({ event: "auth.login.failure", outcome: "failure", reason: "bad_password", actor: { type: "user", id: userId }, source: { ip } }, "auth.login.failure");

Redaction by key name is a backstop, not the plan: the primary control is never passing secrets into the emitter. Do not log raw request bodies on auth routes at all.

4. Instrument sources

  • Wrap the auth library’s hooks (login success and failure, MFA, reset) rather than every route.
  • Emit authz.denied inside the central check() (see authorization-models), with subject, action, resource, and policy version.
  • Admin panels and internal tools are the highest-value audit targets. Every mutation there emits an admin.* event with before/after for the changed fields (minus secrets).
  • Aggregate noisy events (validation rejections) per actor per minute instead of one line each.

5. Storage, tamper resistance, retention

NeedDefault control
Ops and debugging logsNormal log pipeline, 14-30 days
Security eventsSeparate stream or index, 12 months searchable is a common audit expectation
Tamper resistanceShip to a separate account or project the app cannot write to except append; object storage with write-once retention (S3 Object Lock in compliance mode, GCS bucket lock, Azure immutable blob)
Integrity evidenceHash-chain each batch (hash_n = SHA-256(hash_{n-1} || batch_n)) and store the head elsewhere, or rely on provider digests such as CloudTrail log file validation (aws cloudtrail validate-logs)
AccessRead via SIEM roles; deletion only by a break-glass role whose use itself alerts
Personal dataPseudonymous IDs in events; the ID-to-person mapping lives in the main DB under its own retention

An in-database audit_log table is acceptable for product-facing audit history (customers viewing their org’s activity) but is not tamper-resistant on its own: revoke UPDATE and DELETE from the app role and replicate to the immutable sink.

6. Detections

Each detection has: query, threshold, window, owner, runbook link, and expected false positives. Start with these:

DetectionLogic (adapt to your SIEM)Action
Credential stuffingauth.login.failure > 50 distinct usernames from one IP or ASN in 5 min, or failure ratio > 90% over 1,000 attempts globallyPage; enable stricter rate limits (bot-fraud-defense)
Account takeover signalauth.login.success after 5+ failures for same account, from a new country or deviceNotify user; step-up next action
Privilege escalationAny admin.role.granted to owner/admin, or outside business hoursAlert security channel; daily review
Mass data exportdata.export.created rows > 10x actor’s 30-day median, or > N exports in 1 hPage; consider auto-suspend of token
Authorization probingauthz.denied > 20 distinct target IDs per actor in 10 minAlert; likely IDOR enumeration
Audit pipeline silenceZero security events from a service for 15 min during normal trafficPage; logging outage hides attacks
Break-glass useAny use of the emergency admin or log-deletion rolePage, always

Write detections as code where the SIEM allows (Sigma rules, saved searches in Terraform) so they are reviewed and versioned.

Gotchas

  • Logging req.headers wholesale leaks Authorization and Cookie. Allowlist headers.
  • URL query strings carry tokens (password reset, OAuth code, signed URLs). Strip query strings from access logs on sensitive paths.
  • Exceptions from HTTP clients often include the full request, headers and all. Scrub in the error reporter too (Sentry before_send).
  • User-controlled strings in logs enable log injection: JSON encoding handles newlines; in plain-text logs, escape CR/LF.
  • “Username not found” vs “wrong password” in logs is fine; in responses it is enumeration.
  • Client IP behind a proxy: trust X-Forwarded-For only from your own load balancer hops, or the attacker chooses the IP you log.
  • Sampling and log-level filtering must never drop security events. Route them on a channel that is exempt.
  • Retention conflicts: privacy rules push for shorter, audit for longer. Decide per stream and write it down (see gdpr-privacy).

Verification

  1. Unit tests assert that each instrumented action emits exactly one event with required fields (capture the logger output in tests).
  2. Redaction test: send a login with password CanaryP@ss-<random> and an Authorization header with a canary token; grep the full log output and the error tracker for both canaries. Zero hits required.
  3. Tamper test: confirm the app’s runtime role gets AccessDenied deleting or overwriting objects in the audit sink.
  4. Fire each detection in staging with a synthetic burst and confirm the alert reaches the owner with a runbook link.
  5. Tabletop: answer “what did user X do between T1 and T2, and who granted them admin?” with one query each, in under five minutes.

Output contract

## Security logging: <service>
| Event | Emitted at | Fields verified | Test |
|---|---|---|---|
| auth.login.failure | auth/hooks.py:41 | actor, reason, source.ip | test_login_events |
Sink: <collector> -> <SIEM index> (12 mo) + <immutable bucket> (Object Lock, 400 d)
Redaction canary: 0 hits in logs and error tracker
Detections: <name, threshold, owner, runbook> x N; fired in staging on <date>
Gaps: <sources not yet instrumented>

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

references/event-catalog.md 120 lines · 5.6 KB
# Security event catalog

Extended vocabulary with required fields and reason codes. Every event also carries the
common envelope: `ts`, `event`, `outcome`, `actor`, `source`, `request_id`, `trace_id`,
`service`, `env`, `schema_version`.

## Contents

1. Authentication
2. Session and token lifecycle
3. Authorization
4. Administrative and privilege changes
5. Data access and export
6. Configuration and integrations
7. Input and abuse signals
8. Mapping to OCSF and ECS
9. What never to log

---

## 1. Authentication

| Event | Extra required fields | Reason codes |
|---|---|---|
| `auth.login.success` | `method` (password, passkey, sso, magic_link), `mfa_used` (bool), `new_device` (bool) | - |
| `auth.login.failure` | `method`, `username_hash` when no account matched | `bad_password`, `unknown_user`, `locked`, `disabled`, `mfa_required`, `rate_limited`, `sso_error` |
| `auth.mfa.challenge.success` / `.failure` | `factor` (totp, passkey, sms, recovery_code) | `bad_code`, `expired`, `replayed` |
| `auth.lockout.triggered` | `threshold`, `window_seconds` | - |
| `auth.password.reset.requested` | `delivery` (email, sms) | - |
| `auth.password.reset.completed` | `sessions_revoked` (count) | `token_expired`, `token_reused` on failure |
| `auth.sso.assertion.rejected` | `idp`, `issuer` | `bad_signature`, `audience_mismatch`, `expired`, `replay` |

Hash unknown usernames (keyed HMAC, not plain SHA-256) so repeated attempts correlate without
storing what people typed, which is often a password pasted into the wrong field.

## 2. Session and token lifecycle

| Event | Extra required fields |
|---|---|
| `auth.session.created` / `.revoked` | `session_id_hash`, `revoked_by` (self, admin, system), `reason` |
| `auth.credential.added` / `.removed` | `credential_type` (password, passkey, totp, recovery_codes), `credential_id` (non-secret identifier) |
| `apikey.created` / `.revoked` / `.used_first_time` | `key_id` (prefix only, e.g. `sk_live_ab12`), `scopes`, `expires_at` |
| `oauth.grant.created` / `.revoked` | `client_id`, `scopes` |
| `token.refresh.reuse_detected` | `family_id` (refresh token family) |

## 3. Authorization

| Event | Extra required fields |
|---|---|
| `authz.denied` | `action`, `target`, `policy` (name), `policy_version` |
| `authz.elevated` | `action`, `step_up_method` (reauth, passkey) |
| `tenant.boundary.violation` | `actor.org_id`, `target.org_id` - should be zero; any occurrence is a bug or an attack |

## 4. Administrative and privilege changes

| Event | Extra required fields |
|---|---|
| `admin.role.granted` / `.revoked` | `subject` (user receiving the change), `role`, `scope` (org, project) |
| `admin.user.created` / `.disabled` / `.deleted` | `subject` |
| `admin.impersonation.started` / `.ended` | `subject`, `ticket_id`, `duration_seconds` on end |
| `admin.setting.changed` | `setting`, `old_value`, `new_value` (never for secret values; log `rotated: true` instead) |
| `admin.breakglass.used` | `role`, `justification` |

## 5. Data access and export

| Event | Extra required fields |
|---|---|
| `data.export.created` | `resource_type`, `row_count`, `format`, `filter_summary` |
| `data.bulk_read` | `resource_type`, `row_count`, triggered when a single request returns > N rows |
| `data.record.viewed` | Only for regulated records (health, HR, payment): `resource_type`, `target.id` |
| `data.deletion.completed` | `resource_type`, `count`, `request_id` of the DSAR or retention job |

## 6. Configuration and integrations

| Event | Extra required fields |
|---|---|
| `config.security.changed` | `setting` (sso_enforced, mfa_required, ip_allowlist, session_ttl, retention_days), old/new |
| `integration.webhook.signature_failed` | `provider`, `endpoint` - bursts indicate probing or a rotated secret |
| `integration.connected` / `.disconnected` | `provider`, `scopes` |
| `secret.rotated` | `secret_name` (not value), `rotated_by` |

## 7. Input and abuse signals

Aggregate per actor or IP per minute:

| Event | Fields |
|---|---|
| `input.validation.rejected` | `route`, `rule`, `count` |
| `abuse.rate_limited` | `route`, `limit`, `key_type` (ip, user, api_key), `count` |
| `abuse.captcha.failed` | `route`, `count` |
| `upload.rejected` | `reason` (type, size, malware), `content_type_claimed` |

## 8. Mapping to OCSF and ECS

If the SIEM expects a standard schema, map at the collector, not in application code:

| This schema | OCSF (Authentication / API Activity classes) | ECS |
|---|---|---|
| `ts` | `time` | `@timestamp` |
| `event` | `activity_name` plus `class_uid` | `event.action` |
| `outcome` | `status` (Success / Failure) | `event.outcome` |
| `actor.id` | `actor.user.uid` | `user.id` |
| `target.type` / `target.id` | `resources[].type` / `resources[].uid` | custom fields or `related.*` |
| `source.ip` | `src_endpoint.ip` | `source.ip` |
| `source.user_agent` | `http_request.user_agent` | `user_agent.original` |
| `reason` | `status_detail` | `event.reason` |
| `trace_id` | `metadata.correlation_uid` | `trace.id` |

Field names above follow the published schemas at the time of writing; confirm against the
schema version your SIEM ingests.

## 9. What never to log

- Passwords, OTP codes, recovery codes, passkey assertions, private keys.
- Session IDs, bearer tokens, refresh tokens, API keys (log a non-secret prefix or ID).
- Password reset, magic link, and signed download URLs (they are credentials).
- Full payment card numbers, CVV, bank account numbers; government IDs.
- Request and response bodies on auth, payment, and profile routes.
- Health, biometric, or other special-category data unless a regulation requires access
  logging, and then only the record ID.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc