This kit's defaults assume Linux on local ext4/xfs. Other platforms have known gaps. Each section gives a tested compensating control with verification steps.
Gap. chattr +i and chattr +a are no-ops on NFS-mounted filesystems, so
~/.claude.json cannot be made immutable to block claude mcp add, and the
audit log fallback under ~/.claude/audit-fallback.jsonl cannot be made
append-only.
Compensation — push config from SSM, not user-writable home.
- Store the canonical
~/.claude.jsoncontent in SSM Parameter Store:/claude-code/user-claude-json. - At each session start, the wrapper sources
/etc/claude-code/refresh-user-config.shwhich fetches the parameter and writes~/.claude.jsonas0444owned by root via a setuid helper. NFS clients honour the mode bits. - Audit fallback path moves to a local-disk tmpfs: set
CLAUDE_AUDIT_LOG=/var/log/claude-code/audit.jsonl(always local) and rely on the fail-closed behaviour of the rewrittenaudit-logger.sh(v2.0.0). If the local disk is unwritable AND CloudWatch is unreachable, the hook exits 2 — the user gets blocked rather than silently logging to a tampered path.
Verify.
# After deploy:
[[ ! -w ~/.claude.json ]] && echo "config not user-writable" || echo "FAIL"
mount | grep $HOME | grep -q nfs && echo "NFS detected — using SSM fallback"Gap. The UserPromptSubmit hook does not fire when claude is invoked with
--print on Windows (verified, README:183). PII can reach the model on that
path before any local hook gets a chance to inspect it.
Compensation — Bedrock Guardrails as required server-side filter.
- Create a Bedrock Guardrail with PII filtering for the same data classes the local PII guard catches (CC, AWS keys, JWT, NRIC, ...).
- Pass the guardrail headers via
ANTHROPIC_CUSTOM_HEADERSinmanaged-settings.json(this is the mechanism Claude Code reads):See bedrock-guardrails.md for full configuration details. - Add a startup self-check (run as part of the wrapper) that calls
bedrock:GetGuardrailand refuses to launch claude if the Guardrail is missing, in DRAFT-only mode, or has fewer than the expected filter classes. - Add a CloudWatch alarm on
BedrockGuardrails:Intervenedcount -- sustained non-zero means the local PII guard is missing patterns.
Verify.
# Confirm guardrail is active before allowing claude to run:
$guardrailId = "your-guardrail-id"
aws bedrock get-guardrail --guardrail-identifier $guardrailId `
--query 'status' --output text # must be READYGap 1. chflags uchg is removable by the owning user; only chflags schg
(system immutable) plus the user not being a sudoer prevents removal.
Gap 2. Most corporate Macs leave the developer with sudo, defeating
schg-only protection.
Compensation — MDM-enforced non-sudoer + schg.
- Use Jamf / Intune / Mosyle to ensure
claude-code-usersgroup members are not inadmin,wheel, oreveryone-passwordless sudoers entries. The MDM policy must apply at every logon (drift recovery). - Apply
schg:sudo chflags schg ~/.claude.json sudo chflags schg /etc/claude-code/managed-settings.json - Drift watcher (this kit's
drift-watcher.sh) detectschflags noschgattempts viafswatchand alerts within ~60ms (verified, seetests/results/).
Verify.
ls -lO ~/.claude.json | grep -q 'schg' && echo "schg ok" || echo "FAIL: missing schg"
groups $(whoami) | grep -qE '\b(admin|wheel)\b' && echo "FAIL: user is admin" || echo "non-admin ok"Gap. No persistent home for chattr +i; ephemeral pods churn drift state
and audit chain on every restart.
Compensation — config from ConfigMap (read-only mount), audit chain externalised, drift watcher omitted.
- Mount
managed-settings.jsonfrom a ConfigMap asreadOnly: true. The pod cannot write to it regardless of in-container UID. - Mount the hook scripts from a ConfigMap (or build into the image) at a non-writable path.
- The audit chain state (
/var/lib/claude-code/audit-state/last-hmac) is externalised to DynamoDB via a small wrapper aroundaudit-logger.sh. Each pod reads the cluster-global last hash before computing its event so the chain spans pods. - Skip drift-watcher (no on-disk drift to detect — config is RO via the mount). Replace with a Kubernetes admission webhook that rejects pods whose ConfigMap content hash does not match the approved baseline.
This pattern is sketched in terraform/k8s-baseline/ (left as a follow-up).
| Platform / setup | Default control | Compensating control | Verified |
|---|---|---|---|
| Linux ext4/xfs, local home | chattr +i, +a | (none needed) | ✅ |
| Linux NFS home | none (chattr no-op) | SSM-pushed RO config + fail-closed | ☑ design |
| macOS, sudoer user | chflags uchg | MDM non-sudoer + schg + drift watcher | ☑ design |
| macOS, non-sudoer | chflags schg | (matches default Linux behaviour) | ✅ |
| Windows native | UserPromptSubmit gap | Bedrock Guardrails (mandatory) | ☑ design |
| Windows native | no gh/MCP write guard (both hooks are bash) |
Branch protection + required reviews server-side on the remote; deny Bash(gh:*) if WSL is present |
☑ design |
| Kubernetes pod | n/a (ephemeral) | ConfigMap RO + DynamoDB chain state | ☑ design |