Claude Code for Audit Logging: Compliance, Change Tracking, and Forensic Trails — Claude Skills 360 Blog
Blog / Development / Claude Code for Audit Logging: Compliance, Change Tracking, and Forensic Trails
Development

Claude Code for Audit Logging: Compliance, Change Tracking, and Forensic Trails

Published: August 5, 2026
Read time: 8 min read
By: Claude Skills 360

Audit logs are how you answer “who changed what, when, and why?” — required for SOC 2, HIPAA, financial regulations, and security incident response. The challenge is making them immutable (attackers can’t erase their tracks), complete (nothing slips through), and queryable (you can actually find what happened). Claude Code generates audit logging middleware, database triggers for automatic change capture, and the tamper-evident log structures compliance requires.

Audit Log Architecture

Build an audit logging system that captures:
- All data modifications (who, what, before/after values, when)
- All authentication events (login, logout, failed attempts)
- All admin actions (privilege escalation, user management)
Must be tamper-evident and not depend on application code to write it.

Database Trigger Approach (Automatic, Can’t Be Bypassed)

-- audit_log table: append-only (no UPDATE/DELETE allowed via policy)
CREATE TABLE audit_log (
    id          BIGSERIAL PRIMARY KEY,
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    table_name  TEXT NOT NULL,
    record_id   TEXT NOT NULL,
    operation   TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
    user_id     TEXT,                   -- From application session
    ip_address  INET,
    old_values  JSONB,
    new_values  JSONB,
    changed_by  TEXT NOT NULL DEFAULT current_user, -- DB user (service account)
    checksum    TEXT                    -- HMAC of row content for tamper detection
);

-- Prevent deletes and updates to audit_log itself
CREATE RULE audit_log_no_delete AS ON DELETE TO audit_log DO INSTEAD NOTHING;
CREATE RULE audit_log_no_update AS ON UPDATE TO audit_log DO INSTEAD NOTHING;

-- Function that runs on trigger
CREATE OR REPLACE FUNCTION log_changes() RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_log (
        table_name, record_id, operation,
        user_id, ip_address, old_values, new_values
    ) VALUES (
        TG_TABLE_NAME,
        COALESCE(NEW.id::TEXT, OLD.id::TEXT),
        TG_OP,
        current_setting('app.user_id', true),    -- Set by application middleware
        current_setting('app.ip_address', true)::INET,
        CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE to_jsonb(OLD) END,
        CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE to_jsonb(NEW) END
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Attach to tables that need auditing
CREATE TRIGGER audit_users
    AFTER INSERT OR UPDATE OR DELETE ON users
    FOR EACH ROW EXECUTE FUNCTION log_changes();

CREATE TRIGGER audit_orders
    AFTER INSERT OR UPDATE OR DELETE ON orders
    FOR EACH ROW EXECUTE FUNCTION log_changes();

The trigger captures changes regardless of whether the application code logs them — even direct SQL modifications by DBAs are recorded.

Application Context Middleware

The trigger reads app.user_id from session settings — the application must set this:

// middleware/auditContext.ts
export function auditContextMiddleware(req: Request, res: Response, next: NextFunction) {
  // All DB operations in this request will have user context
  db.raw(`
    SELECT set_config('app.user_id', ?, true),
           set_config('app.ip_address', ?, true)
  `, [req.user?.userId ?? 'anonymous', req.ip]).then(() => next());
}

app.use(authenticate);
app.use(auditContextMiddleware);

Application-Level Audit Events

For events that aren’t database changes (logins, exports, permission changes):

// services/AuditService.ts
import { createHmac } from 'crypto';

interface AuditEvent {
  userId: string;
  action: string;
  resourceType?: string;
  resourceId?: string;
  metadata?: Record<string, unknown>;
  ipAddress: string;
  userAgent?: string;
  success: boolean;
  failureReason?: string;
}

export class AuditService {
  private readonly HMAC_KEY = process.env.AUDIT_HMAC_KEY!; // For tamper detection

  async log(event: AuditEvent): Promise<void> {
    const entry = {
      ...event,
      occurred_at: new Date(),
      seq: await this.nextSequenceNumber(),
    };

    // Compute HMAC over content + previous entry's HMAC (hash chain)
    const prevChecksum = await this.getLastChecksum();
    const content = JSON.stringify(entry) + prevChecksum;
    const checksum = createHmac('sha256', this.HMAC_KEY).update(content).digest('hex');

    await db('application_audit_log').insert({ ...entry, checksum });
  }

  // Verify log integrity — detect if any entries were modified or deleted
  async verifyIntegrity(fromId?: number): Promise<{ valid: boolean; firstTamperedId?: number }> {
    const entries = await db('application_audit_log')
      .where('id', '>=', fromId ?? 0)
      .orderBy('id', 'asc');

    let prevChecksum = '';

    for (const entry of entries) {
      const { checksum, ...rest } = entry;
      const content = JSON.stringify(rest) + prevChecksum;
      const expected = createHmac('sha256', this.HMAC_KEY).update(content).digest('hex');

      if (expected !== checksum) {
        return { valid: false, firstTamperedId: entry.id };
      }

      prevChecksum = checksum;
    }

    return { valid: true };
  }

  private async nextSequenceNumber(): Promise<number> {
    const result = await db.raw('SELECT nextval(\'audit_log_seq\')');
    return result.rows[0].nextval;
  }

  private async getLastChecksum(): Promise<string> {
    const last = await db('application_audit_log').orderBy('id', 'desc').first('checksum');
    return last?.checksum ?? '';
  }
}

Authentication Audit Trail

// middleware/authAudit.ts — log all auth events
export async function logAuthEvent(
  type: 'login' | 'logout' | 'login_failed' | 'password_reset' | 'mfa_failed',
  userId: string | null,
  req: Request,
  extra?: Record<string, unknown>,
) {
  await audit.log({
    userId: userId ?? 'anonymous',
    action: type,
    resourceType: 'auth',
    metadata: {
      ...extra,
      userAgent: req.headers['user-agent'],
    },
    ipAddress: req.ip,
    success: !type.includes('failed'),
    failureReason: type.includes('failed') ? type : undefined,
  });
}

// In your login handler:
app.post('/auth/login', async (req, res) => {
  try {
    const user = await authenticate(req.body.email, req.body.password);
    await logAuthEvent('login', user.id, req);
    // ...
  } catch (err) {
    await logAuthEvent('login_failed', null, req, { email: req.body.email });
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

Audit Log Query API

// api/audit/query.ts — allow admins to search the audit trail
export async function GET(request: Request) {
  const session = await requireAdminSession(request);
  const { searchParams } = new URL(request.url);

  const query = db('audit_log')
    .select('*')
    .orderBy('occurred_at', 'desc');

  if (searchParams.get('userId')) {
    query.where('user_id', searchParams.get('userId'));
  }

  if (searchParams.get('resourceId')) {
    query.where('record_id', searchParams.get('resourceId'));
  }

  if (searchParams.get('from')) {
    query.where('occurred_at', '>=', new Date(searchParams.get('from')!));
  }

  if (searchParams.get('to')) {
    query.where('occurred_at', '<=', new Date(searchParams.get('to')!));
  }

  const page = parseInt(searchParams.get('page') ?? '1');
  const limit = Math.min(parseInt(searchParams.get('limit') ?? '50'), 200);

  const [events, [{ count }]] = await Promise.all([
    query.clone().limit(limit).offset((page - 1) * limit),
    query.clone().count('* as count'),
  ]);

  return Response.json({ events, total: Number(count), page, limit });
}

CLAUDE.md for Compliance Requirements

## Audit Logging Requirements (SOC 2 Type II)
- All PII modifications: logged via DB triggers (automatic)
- Authentication events: logged via authAudit middleware
- Admin actions: must call AuditService.log() explicitly
- Log retention: 7 years (HIPAA) / 3 years (SOC 2) — enforced by lifecycle policy
- Export: audit logs available via admin API /audit/export?from=&to= (date range)
- Integrity verification: run weekly via scheduled job, alert on failure
- Never log passwords, tokens, or credit card numbers in audit fields
- Before/after diffs: DB triggers capture these automatically

For the security testing that validates your audit trail catches all attack vectors, see the security testing guide. For the observability layer that alerts on suspicious audit patterns, see the OpenTelemetry guide. The Claude Skills 360 bundle includes compliance skill sets for SOC 2, HIPAA, and tamper-evident audit logging. Start with the free tier to try audit system scaffolding.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free