|
| 1 | +# auth-rotation-reminder |
| 2 | + |
| 3 | +Claude Code `Stop` hook that periodically logs you out of authenticated |
| 4 | +CLIs (npm, pnpm, gcloud, vault, aws sso, docker, socket, …) so stale |
| 5 | +long-lived tokens don't sit in your dotfiles or keychain for days. |
| 6 | + |
| 7 | +## Why |
| 8 | + |
| 9 | +Long-lived auth tokens live in well-known locations: `~/.npmrc`, |
| 10 | +`~/.config/gh/hosts.yml`, `~/.config/gcloud/`, `~/.docker/config.json`. |
| 11 | +A compromised dev workstation has a wide blast radius on those files. |
| 12 | +Periodic auto-revocation tightens the window and forces explicit |
| 13 | +re-authentication, which is itself a small phishing-defense moment |
| 14 | +("did I really mean to publish?"). |
| 15 | + |
| 16 | +## Defaults |
| 17 | + |
| 18 | +- **Interval**: 1 hour. Set `SOCKET_AUTH_ROTATION_INTERVAL_HOURS=4` to |
| 19 | + loosen, `=0` to run on every Stop event. |
| 20 | +- **Mode**: auto-logout (the hook *acts*, not just warns). |
| 21 | +- **Default skip-list**: `gh` is skipped because Claude Code itself |
| 22 | + uses `gh` for `gh pr edit` etc. — auto-revoking it would break the |
| 23 | + agent. |
| 24 | +- **CI**: hook short-circuits when `CI` env var is set. |
| 25 | + |
| 26 | +## What's swept |
| 27 | + |
| 28 | +| id | display name | detect | logout | |
| 29 | +| --------- | ----------------- | ----------------- | ------------------------------ | |
| 30 | +| npm | npm | `npm whoami` | `npm logout` | |
| 31 | +| pnpm | pnpm | `pnpm whoami` | `pnpm logout` | |
| 32 | +| yarn | yarn | `yarn --version` | `yarn npm logout` | |
| 33 | +| gcloud | gcloud | `gcloud auth list ... ACTIVE` | `gcloud auth revoke --all --quiet` | |
| 34 | +| aws-sso | aws (sso) | `aws sts get-caller-identity` | `aws sso logout` | |
| 35 | +| gh | gh (GitHub CLI) | `gh auth status` | `gh auth logout --hostname github.com` | |
| 36 | +| vault | vault | `vault token lookup` | `vault token revoke -self` | |
| 37 | +| docker | docker | `docker info \| grep Username:` | `docker logout` | |
| 38 | +| socket | socket | `socket whoami` | `socket logout` | |
| 39 | + |
| 40 | +The hook never reads, prints, or compares any token value. Detection |
| 41 | +is exit-code only; logout commands' output is suppressed except for |
| 42 | +non-zero exit codes which surface as "logout failed" lines. |
| 43 | + |
| 44 | +## Snoozing |
| 45 | + |
| 46 | +Need to keep your auth alive for the next few hours (e.g. mid-publish)? |
| 47 | +Drop a `.snooze` file with an ISO 8601 expiry on line 1. |
| 48 | + |
| 49 | +```bash |
| 50 | +# Snooze for 4 hours, project-local |
| 51 | +date -ud "+4 hours" +"%Y-%m-%dT%H:%M:%SZ" > .claude/auth-rotation.snooze |
| 52 | + |
| 53 | +# Snooze globally for 8 hours (applies to every repo) |
| 54 | +mkdir -p ~/.claude/hooks/auth-rotation |
| 55 | +date -ud "+8 hours" +"%Y-%m-%dT%H:%M:%SZ" > ~/.claude/hooks/auth-rotation/snooze |
| 56 | +``` |
| 57 | + |
| 58 | +The hook **automatically deletes the file** once the timestamp is |
| 59 | +reached. No manual cleanup needed. |
| 60 | + |
| 61 | +Snoozes that are malformed, empty, or unreadable are also auto-deleted |
| 62 | +on the next run — fail-safe so a corrupted file can't permanently |
| 63 | +disable rotation. |
| 64 | + |
| 65 | +`.claude/*.snooze` is gitignored; project-local snoozes never leak into |
| 66 | +commits. |
| 67 | + |
| 68 | +## Skip-list |
| 69 | + |
| 70 | +Permanently skip a service: |
| 71 | + |
| 72 | +```bash |
| 73 | +# Per-user: applies to every repo |
| 74 | +mkdir -p ~/.claude/hooks/auth-rotation |
| 75 | +echo gcloud >> ~/.claude/hooks/auth-rotation/services-skip |
| 76 | + |
| 77 | +# Per-repo: applies just to this checkout |
| 78 | +echo vault >> .claude/auth-rotation.services-skip |
| 79 | +``` |
| 80 | + |
| 81 | +One id per line. Lines starting with `#` are comments. Service ids |
| 82 | +are stable — see the table above. |
| 83 | + |
| 84 | +## Disable temporarily |
| 85 | + |
| 86 | +```bash |
| 87 | +SOCKET_AUTH_ROTATION_DISABLED=1 # any non-empty value |
| 88 | +``` |
| 89 | + |
| 90 | +For pairing sessions, demos, etc. The hook short-circuits before |
| 91 | +doing any work. |
| 92 | + |
| 93 | +## Wiring |
| 94 | + |
| 95 | +In `.claude/settings.json`: |
| 96 | + |
| 97 | +```json |
| 98 | +{ |
| 99 | + "hooks": { |
| 100 | + "Stop": [ |
| 101 | + { |
| 102 | + "hooks": [ |
| 103 | + { |
| 104 | + "type": "command", |
| 105 | + "command": "node .claude/hooks/auth-rotation-reminder/index.mts" |
| 106 | + } |
| 107 | + ] |
| 108 | + } |
| 109 | + ] |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +## Tests |
| 115 | + |
| 116 | +```bash |
| 117 | +cd .claude/hooks/auth-rotation-reminder |
| 118 | +node --test test/*.test.mts |
| 119 | +``` |
| 120 | + |
| 121 | +## Reusing the snooze convention |
| 122 | + |
| 123 | +Other hooks can adopt the same `.snooze` pattern. The convention is: |
| 124 | + |
| 125 | +- Filename: `.claude/<hook-id>.snooze` (project) or |
| 126 | + `~/.claude/hooks/<hook-id>/snooze` (global). |
| 127 | +- Format: ISO 8601 expiry on line 1. Optional further lines ignored. |
| 128 | +- `.gitignore`: `.claude/*.snooze`. |
| 129 | +- Cleanup: hook auto-deletes expired files via `safeDelete`. |
| 130 | +- The `checkSnoozes` / `tryUnlink` helpers in `index.mts` are easy to |
| 131 | + copy into a sibling hook. |
0 commit comments