feat(audit): log external docker stop/rm on claude-hooks-* via auditd #150

Merged
code-lead merged 1 commit from boss/149 into main 2026-04-20 14:33:07 +00:00
Collaborator

Summary

Closes #149 (and closes the diagnostic gap from #132). When
dev-default silently vanishes we had no way to attribute the
docker stop / docker rm call to a caller — dockerd logs the stop
but not who triggered it, and everything service-side checked out
clean. This ships an auditd rule that logs every docker CLI execute
by uid 0 or uid 1000 under the key claude-hooks-docker, capturing
PID, full argv, exe path, and cwd.

Kernel rules can't filter on argv, so the rule fires on every docker
invocation by root or the operator; the stop/rm + claude-hooks-*
narrowing happens post-hoc via ausearch. Noise is acceptable and the
audit key makes it a one-liner to extract only the events we care
about.

Changes

  • ops/audit/claude-hooks-docker.rules — two auditd rules (uid 0
    and uid 1000) on path=@@DOCKER_PATH@@ with perm=x. The
    placeholder is rewritten at install time so the rule matches the
    host's real docker path.
  • justfile — three new recipes:
    • audit-install — sed the placeholder, sudo install to
      /etc/audit/rules.d/claude-hooks-docker.rules, augenrules --load.
    • audit-tail — polls ausearch --checkpoint every 2 s (ausearch
      has no native follow mode). First call scopes to -ts today so we
      don't dump history.
    • audit-uninstall — remove the rule file and reload. Idempotent.
  • README.md — new "Debugging with auditd" section: install, read
    events, follow mode, uninstall, scope caveats.
  • CLAUDE.md — Commands section now lists the three recipes.

Test plan

  • just audit-install on charles-desktop → verify rule present
    via sudo auditctl -l | grep claude-hooks-docker.
  • Trigger docker stop claude-hooks-<test-instance> manually →
    sudo ausearch -k claude-hooks-docker -ts today -i shows the
    operator's shell PID, argv (a0=docker a1=stop a2=claude-hooks-…), exe, and cwd.
  • Trigger via subprocess (bash -c 'docker stop claude-hooks-…')
    → parent + child PIDs both logged.
  • just audit-tail in one terminal + docker rm in another →
    new event shows up within 2 s.
  • Wait for (or force) another dev-default disappearance;
    inspect the audit log, identify the caller, paste the finding
    into a comment on #132 and close that ticket (per #149 AC).
  • just audit-uninstall → rule file gone, auditctl -l clean.

Closes #149

## Summary Closes #149 (and closes the diagnostic gap from #132). When `dev-default` silently vanishes we had no way to attribute the `docker stop` / `docker rm` call to a caller — dockerd logs the stop but not who triggered it, and everything service-side checked out clean. This ships an auditd rule that logs every `docker` CLI execute by uid 0 or uid 1000 under the key `claude-hooks-docker`, capturing PID, full argv, exe path, and cwd. Kernel rules can't filter on argv, so the rule fires on every docker invocation by root or the operator; the `stop`/`rm` + `claude-hooks-*` narrowing happens post-hoc via `ausearch`. Noise is acceptable and the audit key makes it a one-liner to extract only the events we care about. ## Changes - **`ops/audit/claude-hooks-docker.rules`** — two auditd rules (uid 0 and uid 1000) on `path=@@DOCKER_PATH@@` with `perm=x`. The placeholder is rewritten at install time so the rule matches the host's real docker path. - **`justfile`** — three new recipes: - `audit-install` — sed the placeholder, `sudo install` to `/etc/audit/rules.d/claude-hooks-docker.rules`, `augenrules --load`. - `audit-tail` — polls `ausearch --checkpoint` every 2 s (ausearch has no native follow mode). First call scopes to `-ts today` so we don't dump history. - `audit-uninstall` — remove the rule file and reload. Idempotent. - **`README.md`** — new "Debugging with auditd" section: install, read events, follow mode, uninstall, scope caveats. - **`CLAUDE.md`** — Commands section now lists the three recipes. ## Test plan - [ ] `just audit-install` on charles-desktop → verify rule present via `sudo auditctl -l | grep claude-hooks-docker`. - [ ] Trigger `docker stop claude-hooks-<test-instance>` manually → `sudo ausearch -k claude-hooks-docker -ts today -i` shows the operator's shell PID, argv (`a0=docker a1=stop a2=claude-hooks-…`), exe, and cwd. - [ ] Trigger via subprocess (`bash -c 'docker stop claude-hooks-…'`) → parent + child PIDs both logged. - [ ] `just audit-tail` in one terminal + `docker rm` in another → new event shows up within 2 s. - [ ] Wait for (or force) another dev-default disappearance; inspect the audit log, identify the caller, paste the finding into a comment on #132 and close that ticket (per #149 AC). - [ ] `just audit-uninstall` → rule file gone, `auditctl -l` clean. Closes #149
feat(audit): log external docker stop/rm on claude-hooks-* via auditd (#149)
All checks were successful
qa / qa (pull_request) Successful in 3m4s
qa / dockerfile (pull_request) Successful in 11s
b31c20eb13
Closes the diagnostic gap from #132: when dev-default silently vanishes
we had no way to attribute the `docker stop` / `docker rm` call to a
caller. Ships an auditd rule at ops/audit/claude-hooks-docker.rules that
logs every docker CLI execute by uid 0 or uid 1000 with PID, argv, exe,
and cwd under the `claude-hooks-docker` key.

- `just audit-install` rewrites @@DOCKER_PATH@@ to the host's real
  `command -v docker`, installs to /etc/audit/rules.d/, and reloads.
- `just audit-tail` follows new events via ausearch --checkpoint polling
  (ausearch has no native follow mode).
- `just audit-uninstall` removes the rule file and reloads.
- README grows a "Debugging with auditd" section; CLAUDE.md lists the
  three new recipes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-04-20 14:32:04 +00:00
Dismissed
reviewer left a comment

Review: feat(audit): log external docker stop/rm on claude-hooks-* via auditd

CI: green (run #1740, sha b31c20e, 3m17s)
Round: 1 (no prior reviews by me)


Acceptance criteria check

AC Status
ops/audit/claude-hooks-docker.rules with -k claude-hooks-docker key
@@DOCKER_PATH@@ placeholder resolved at install time via command -v docker
uid 0 and uid 1000 scoped
just audit-install: copies to /etc/audit/rules.d/, runs augenrules --load, prints confirmation + ausearch command
just audit-tail: checkpoint-based polling, scopes first scan to today
just audit-uninstall: idempotent remove + reload (bonus: AC did not require it)
README "Debugging with auditd" section
CLAUDE.md Commands section updated

Code review

ops/audit/claude-hooks-docker.rules — Two -a always,exit -F path=@@DOCKER_PATH@@ -F perm=x -F uid={0,1000} -F key=claude-hooks-docker rules. Correct auditd syntax; perm=x captures execve so full argv (a0=docker, a1=stop|rm, a2=claude-hooks-…) lands in the EXECVE record. The acknowledged design choice to fire on all docker invocations and filter post-hoc is the right approach — kernel rules cannot match argv at dispatch time.

audit-install — Preflight check for auditctl/augenrules/ausearch, then command -v docker || true with an empty-check guard; mktemp+trap cleanup of the rendered temp file; sudo install -o root -g root -m 0640. All correct. augenrules --load stderr is live (not suppressed), so failures surface through set -euo pipefail cleanly.

audit-tail — Checkpoint file under ${XDG_STATE_HOME:-$HOME/.local/state}/claude-hooks/. First run scopes to -ts today, subsequent polls use --checkpoint only. || true on ausearch correctly absorbs its exit code 1 (no events found) while keeping the loop alive. Correct and well-commented.

audit-uninstall — Idempotent: skips removal if file absent, only calls augenrules if the binary exists. Clean.

One non-blocking observation

The issue AC asked for audit-tail output "filtered to the fields operators actually need: timestamp, PID, exe, argv, cwd". The implementation emits full ausearch -i output (all record types: SYSCALL, CWD, EXECVE, PROCTITLE). In practice this is a net positive for debugging — more context, not less — and the README already tells the operator which fields to look for. Not requesting changes; noted for completeness.

## Review: feat(audit): log external docker stop/rm on claude-hooks-* via auditd **CI**: ✅ green (run #1740, sha `b31c20e`, 3m17s) **Round**: 1 (no prior reviews by me) --- ### Acceptance criteria check | AC | Status | |---|---| | `ops/audit/claude-hooks-docker.rules` with `-k claude-hooks-docker` key | ✅ | | `@@DOCKER_PATH@@` placeholder resolved at install time via `command -v docker` | ✅ | | uid 0 and uid 1000 scoped | ✅ | | `just audit-install`: copies to `/etc/audit/rules.d/`, runs `augenrules --load`, prints confirmation + ausearch command | ✅ | | `just audit-tail`: checkpoint-based polling, scopes first scan to today | ✅ | | `just audit-uninstall`: idempotent remove + reload | ✅ (bonus: AC did not require it) | | README "Debugging with auditd" section | ✅ | | CLAUDE.md Commands section updated | ✅ | ### Code review **`ops/audit/claude-hooks-docker.rules`** — Two `-a always,exit -F path=@@DOCKER_PATH@@ -F perm=x -F uid={0,1000} -F key=claude-hooks-docker` rules. Correct auditd syntax; `perm=x` captures `execve` so full argv (a0=docker, a1=stop|rm, a2=claude-hooks-…) lands in the EXECVE record. The acknowledged design choice to fire on all docker invocations and filter post-hoc is the right approach — kernel rules cannot match argv at dispatch time. **`audit-install`** — Preflight check for `auditctl`/`augenrules`/`ausearch`, then `command -v docker || true` with an empty-check guard; `mktemp`+`trap` cleanup of the rendered temp file; `sudo install -o root -g root -m 0640`. All correct. `augenrules --load` stderr is live (not suppressed), so failures surface through `set -euo pipefail` cleanly. **`audit-tail`** — Checkpoint file under `${XDG_STATE_HOME:-$HOME/.local/state}/claude-hooks/`. First run scopes to `-ts today`, subsequent polls use `--checkpoint` only. `|| true` on `ausearch` correctly absorbs its exit code 1 (no events found) while keeping the loop alive. Correct and well-commented. **`audit-uninstall`** — Idempotent: skips removal if file absent, only calls `augenrules` if the binary exists. Clean. ### One non-blocking observation The issue AC asked for `audit-tail` output "filtered to the fields operators actually need: timestamp, PID, exe, argv, cwd". The implementation emits full `ausearch -i` output (all record types: SYSCALL, CWD, EXECVE, PROCTITLE). In practice this is a net positive for debugging — more context, not less — and the README already tells the operator which fields to look for. Not requesting changes; noted for completeness.
reviewer approved these changes 2026-04-20 14:32:15 +00:00
reviewer left a comment

Review: feat(audit): log external docker stop/rm on claude-hooks-* via auditd

CI: green (run #1740, sha b31c20e, 3m17s)
Round: 1 (no prior reviews by me)


Acceptance criteria check

AC Status
ops/audit/claude-hooks-docker.rules with -k claude-hooks-docker key
@@DOCKER_PATH@@ placeholder resolved at install time via command -v docker
uid 0 and uid 1000 scoped
just audit-install: copies to /etc/audit/rules.d/, runs augenrules --load, prints confirmation + ausearch command
just audit-tail: checkpoint-based polling, scopes first scan to today
just audit-uninstall: idempotent remove + reload (bonus: AC did not require it)
README "Debugging with auditd" section
CLAUDE.md Commands section updated

Code review

ops/audit/claude-hooks-docker.rules — Two -a always,exit -F path=@@DOCKER_PATH@@ -F perm=x -F uid={0,1000} -F key=claude-hooks-docker rules. Correct auditd syntax; perm=x captures execve so full argv (a0=docker, a1=stop|rm, a2=claude-hooks-…) lands in the EXECVE record. The acknowledged design choice to fire on all docker invocations and filter post-hoc is the right approach — kernel rules cannot match argv at dispatch time.

audit-install — Preflight check for auditctl/augenrules/ausearch, then command -v docker || true with an empty-check guard; mktemp+trap cleanup of the rendered temp file; sudo install -o root -g root -m 0640. All correct. augenrules --load stderr is live (not suppressed), so failures surface through set -euo pipefail cleanly.

audit-tail — Checkpoint file under ${XDG_STATE_HOME:-$HOME/.local/state}/claude-hooks/. First run scopes to -ts today, subsequent polls use --checkpoint only. || true on ausearch correctly absorbs its exit code 1 (no events found) while keeping the loop alive. Correct and well-commented.

audit-uninstall — Idempotent: skips removal if file absent, only calls augenrules if the binary exists. Clean.

One non-blocking observation

The issue AC asked for audit-tail output "filtered to the fields operators actually need: timestamp, PID, exe, argv, cwd". The implementation emits full ausearch -i output (all record types: SYSCALL, CWD, EXECVE, PROCTITLE). In practice this is a net positive for debugging — more context, not less — and the README already tells the operator which fields to look for. Not requesting changes; noted for completeness.

## Review: feat(audit): log external docker stop/rm on claude-hooks-* via auditd **CI**: ✅ green (run #1740, sha `b31c20e`, 3m17s) **Round**: 1 (no prior reviews by me) --- ### Acceptance criteria check | AC | Status | |---|---| | `ops/audit/claude-hooks-docker.rules` with `-k claude-hooks-docker` key | ✅ | | `@@DOCKER_PATH@@` placeholder resolved at install time via `command -v docker` | ✅ | | uid 0 and uid 1000 scoped | ✅ | | `just audit-install`: copies to `/etc/audit/rules.d/`, runs `augenrules --load`, prints confirmation + ausearch command | ✅ | | `just audit-tail`: checkpoint-based polling, scopes first scan to today | ✅ | | `just audit-uninstall`: idempotent remove + reload | ✅ (bonus: AC did not require it) | | README "Debugging with auditd" section | ✅ | | CLAUDE.md Commands section updated | ✅ | ### Code review **`ops/audit/claude-hooks-docker.rules`** — Two `-a always,exit -F path=@@DOCKER_PATH@@ -F perm=x -F uid={0,1000} -F key=claude-hooks-docker` rules. Correct auditd syntax; `perm=x` captures `execve` so full argv (a0=docker, a1=stop|rm, a2=claude-hooks-…) lands in the EXECVE record. The acknowledged design choice to fire on all docker invocations and filter post-hoc is the right approach — kernel rules cannot match argv at dispatch time. **`audit-install`** — Preflight check for `auditctl`/`augenrules`/`ausearch`, then `command -v docker || true` with an empty-check guard; `mktemp`+`trap` cleanup of the rendered temp file; `sudo install -o root -g root -m 0640`. All correct. `augenrules --load` stderr is live (not suppressed), so failures surface through `set -euo pipefail` cleanly. **`audit-tail`** — Checkpoint file under `${XDG_STATE_HOME:-$HOME/.local/state}/claude-hooks/`. First run scopes to `-ts today`, subsequent polls use `--checkpoint` only. `|| true` on `ausearch` correctly absorbs its exit code 1 (no events found) while keeping the loop alive. Correct and well-commented. **`audit-uninstall`** — Idempotent: skips removal if file absent, only calls `augenrules` if the binary exists. Clean. ### One non-blocking observation The issue AC asked for `audit-tail` output "filtered to the fields operators actually need: timestamp, PID, exe, argv, cwd". The implementation emits full `ausearch -i` output (all record types: SYSCALL, CWD, EXECVE, PROCTITLE). In practice this is a net positive for debugging — more context, not less — and the README already tells the operator which fields to look for. Not requesting changes; noted for completeness.
code-lead deleted branch boss/149 2026-04-20 14:33:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!150
No description provided.