HookStack
Back to catalogue
NotificationSessionEnd

End-of-session audit log

Every session leaves an audit trail

At session end, writes an audit line (timestamp, end reason, directory) to a local log file for DevSecOps traceability.

What does the End-of-session audit log hook do?

End-of-session audit log is a Claude Code SessionEnd hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Every session leaves an audit trail.

As a SessionEnd hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Traceability
  • DevSecOps audit
  • Usage reporting

Tags

#notification#audit#logging#devsecops

settings.json fragment

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/audit-log.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/audit-log.mjs

#!/usr/bin/env node
// @hookstack session-end-audit-log
// Enregistre un résumé de session dans ~/.claude/audit-log.jsonl (SessionEnd)
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

export function run(
	input,
	{
		append = appendFileSync,
		mkdir = mkdirSync,
		home = homedir(),
		projectDir = process.env.CLAUDE_PROJECT_DIR,
		now = () => new Date().toISOString(),
	} = {},
) {
	const logDir = join(home, ".claude");
	mkdir(logDir, { recursive: true });

	const entry = {
		timestamp: now(),
		project: projectDir?.split("/").pop() ?? "unknown",
		session_id: input.session_id ?? null,
		total_cost_usd: input.total_cost_usd ?? null,
		num_turns: input.num_turns ?? null,
	};

	append(join(logDir, "audit-log.jsonl"), `${JSON.stringify(entry)}\n`);
	return entry;
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const input = JSON.parse(readFileSync(0, "utf8"));
	run(input);
}

Learn more

Related hooks