HookStackGitHub
Back to catalogue
WorkflowPostToolUse· BashPostToolUseAfter tool execution · non-blocking· non-blocking

Bash command log

Records every Bash command executed by Claude into a timestamped log file for audit, debugging and a history of the session's actions.

Use cases

  • Audit all shell commands executed by Claude
  • Debug Claude Code sessions after the fact
  • Action history for compliance or documentation

Providers & tags

Claude Code
#logging#bash#audit#history

settings.json fragment

{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/bash-command-log.mjs",
            "type": "command"
          }
        ],
        "matcher": "Bash"
      }
    ]
  }
}

Script · .claude/hooks/bash-command-log.mjs

#!/usr/bin/env node
// Journalise toutes les commandes Bash exécutées (PostToolUse Bash)
import { readFileSync, appendFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';

export function run(
  input,
  {
    append = appendFileSync,
    mkdir = mkdirSync,
    projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
    cwd = process.cwd(),
    now = () => new Date().toISOString(),
  } = {},
) {
  const command = input.tool_input?.command ?? '';
  if (!command) return null;

  const logDir = join(projectDir, '.claude', 'data');
  mkdir(logDir, { recursive: true });

  const entry = {
    ts: now(),
    cmd: command.slice(0, 1000),
    exit: input.tool_response?.exit_code ?? null,
    cwd,
  };

  append(join(logDir, 'bash-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);
}