Skip to content

SOPs

claude -p Credentials — AWS Parameter Store

claude -p exits non-zero (or prints to stdout via the JSON envelope), saying:

Not logged in · Please run /login

or, when the CLI has stale credentials and the server has rotated them:

Failed to authenticate. API Error: 401 Invalid authentication credentials

When this happens, every autonomous caller of claude -p silently fails — session namer, voice nudge classifier, screenshot renamer, Reddit listener, dozens of skill scripts, every launchd cron job that uses Claude.

The sneaky part: nothing alerts. Sessions stop getting names. Screenshots land with raw camera-default filenames. The listener’s classifier returns nothing and the pipeline silently does no work.

┌─────────────────────────────────────────────────────────────────┐
│ AWS Parameter Store (us-west-2) │
│ /claude/oauth/credentials SecureString ←── single source │
└─────────────────────────────────────────────────────────────────┘
▲ writer │ reader
│ (only Studio + MBP) │ (every machine)
│ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ launchd WatchPaths │ │ claude-p-wrapper │
│ watches │ │ on auth_failure detect: │
│ ~/.claude/.credentials.json │ 1. run pull script │
│ → run publish script │ │ 2. retry claude -p once│
│ on every change │ │ 3. log + alert if │
│ (= every /login, │ │ retry still fails │
│ every refresh) │ └──────────────────────────┘
└──────────────────────┘

Auto-publish, auto-pull, auto-recover. Nothing requires James to copy a file, rerun a script, or remember a procedure.

The claude binary checks credentials in this order:

  1. CLAUDE_CODE_OAUTH_TOKEN env var
  2. ANTHROPIC_API_KEY env var
  3. ~/.claude/.credentials.json file
  4. macOS keychain entry Claude Code-credentials

We use (3) as the on-machine source. The keychain (4) auto-locks per-security-session and breaks every headless context; the file does not. Parameter Store keeps the file fresh.

IAM (account 874393260228, region us-west-2)

Section titled “IAM (account 874393260228, region us-west-2)”

Two scoped IAM users, both restricted to the single ARN arn:aws:ssm:us-west-2:874393260228:parameter/claude/oauth/credentials:

UserPolicyWhat it can doLives on
claude-creds-readerclaude-creds-reader-policyssm:GetParameter, kms:Decrypt via SSMStudio, MBP, Remote Mac, VPS
claude-creds-writerclaude-creds-writer-policyadds ssm:PutParameter, kms:Encrypt via SSMStudio + MBP only

The split exists so a compromise of a reader-only machine (Remote Mac, VPS) cannot inject a malicious token back into the canonical store. Audit trail in CloudTrail also distinguishes reader vs writer activity.

AWS Profiles (~/.aws/credentials per machine)

Section titled “AWS Profiles (~/.aws/credentials per machine)”
  • [claude-creds-reader] on all four machines
  • [claude-creds-writer] on Studio + MBP only

Profiles live in the local credentials file with chmod 600. The writer key is never published to secrets.json or any synced surface — losing it costs one IAM delete-access-key + create-access-key cycle to rotate.

  • claude-creds-publish.sh — reads ~/.claude/.credentials.json, runs aws ssm put-parameter with the writer profile. Validates JSON before publishing, refuses to push garbage, logs every result to ~/apps/cc/logs/claude-creds.log.
  • claude-creds-pull.shaws ssm get-parameter --with-decryption with the reader profile, strips the trailing newline that --output text appends, validates JSON, writes to ~/.claude/.credentials.json with chmod 600.
  • _claude-creds-common.sh — shared helpers: find_aws (handles brew-on-Mac vs pip-on-Linux vs ~/.local/bin symlink on Remote Mac), log_event, send_alert (Resend via gmail-notify/send-email.py).

~/Library/LaunchAgents/com.cc.claude-creds-publisher.plist uses WatchPaths on ~/.claude/.credentials.json. Any change to the file — first creation, claude /login overwrite, token refresh — fires the publisher within ~10 seconds. ThrottleInterval=10 prevents burst-fire from rapid writes.

The plist source is at ~/apps/cc/launchd/com.cc.claude-creds-publisher.plist. To install on a new writer machine:

Terminal window
cp ~/apps/cc/launchd/com.cc.claude-creds-publisher.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.cc.claude-creds-publisher.plist
launchctl list | grep claude-creds # verify

Auto-Recover (claude-p-wrapper, every machine)

Section titled “Auto-Recover (claude-p-wrapper, every machine)”

~/apps/claude-p-wrapper/claude-p Build 10+ detects both auth-failure shapes:

  • Shape 1 — JSON envelope: {"is_error":true,"result":"Not logged in · Please run /login",...} (stale credentials)
  • Shape 2 — plain text: Not logged in · Please run /login to stdout (no credentials file at all)

On either detection, the wrapper runs claude-creds-pull.sh, retries the underlying claude -p call once, and returns the retry’s output if successful. If pull or retry fails, it falls through to the existing email-alert + exit-78 path.

Each log line gets two fields: recovery_attempted and recovery_succeeded.

Set CLAUDE_P_NO_RECOVER=1 to bypass — used by tests that need to observe raw auth-failure behavior.

Terminal window
mv ~/.claude/.credentials.json /tmp/backup.json
python3 ~/apps/claude-p-wrapper/claude-p "reply with single word ok" < /dev/null

Expected:

  • stderr: [claude-p] auth lapsed; pulled fresh credentials from Parameter Store, retrying once.
  • stdout: ok
  • ~/.claude/.credentials.json now exists with mode 600

Cleanup (the auto-pulled file is the real current credential — keeping it is fine):

Terminal window
rm /tmp/backup.json

If the test prints Not logged in instead of ok, check:

  1. ~/.aws/credentials on this machine has [claude-creds-reader]
  2. aws --profile claude-creds-reader sts get-caller-identity succeeds
  3. ~/apps/cc/logs/claude-creds.log for the most recent pull error

Reader-only:

Terminal window
# 1. Install awscli (one of these, depending on platform)
brew install awscli # Mac with brew
pip3 install --user awscli # Mac without brew → ~/Library/Python/<ver>/bin/aws (symlink to ~/.local/bin)
pip3 install --break-system-packages awscli # Debian/Ubuntu VPS as root
# 2. Drop the reader profile (Studio or MBP has the access key in their ~/.aws/credentials)
mkdir -p ~/.aws && chmod 700 ~/.aws
cat >> ~/.aws/credentials <<'EOF'
[claude-creds-reader]
aws_access_key_id = AKIA… ← copy from a writer machine
aws_secret_access_key = …
region = us-west-2
EOF
chmod 600 ~/.aws/credentials
# 3. Smoke-test
aws ssm get-parameter --name /claude/oauth/credentials --with-decryption --profile claude-creds-reader --query 'Parameter.Version' --output text

Writer-additional (Studio or MBP only — never on Remote Mac or VPS):

Terminal window
cat >> ~/.aws/credentials <<'EOF'
[claude-creds-writer]
aws_access_key_id = AKIA…
aws_secret_access_key = …
region = us-west-2
EOF
chmod 600 ~/.aws/credentials
# Install the publisher
cp ~/apps/cc/launchd/com.cc.claude-creds-publisher.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.cc.claude-creds-publisher.plist

claude /login (or any access-token refresh) overwrites ~/.claude/.credentials.json. The launchd publisher fires, pushes the new value to Parameter Store, version increments. All four machines now have access to the fresh credential the moment any of them tries to claude -p and fails — auto-recovery pulls it.

You do not need to do anything. The whole point.

To rotate the writer key (e.g., if a writer machine is decommissioned):

Terminal window
# Studio
aws iam list-access-keys --user-name claude-creds-writer
aws iam create-access-key --user-name claude-creds-writer # save AccessKeyId + SecretAccessKey
# Edit ~/.aws/credentials on Studio + MBP, replace the [claude-creds-writer] block
aws iam delete-access-key --user-name claude-creds-writer --access-key-id <OLD_ID>

Reader rotation is identical with claude-creds-reader and writing to all four machines.

The previous design (2026-05-13) snapshotted the credential into secrets.json as CLAUDE_OAUTH_CREDENTIALS. Problems:

  1. No automation kept it fresh. When the OAuth refresh token rotated server-side, the snapshot became invalid and no one knew until claude -p started failing across the fleet.
  2. No auto-recovery. A machine with a stale file had no mechanism to fetch a fresh one.
  3. secrets.json is broadly synced. Every entry on Studio scp’s to MBP, Remote Mac, and VPS. Writer-level capability there violates least privilege.

Parameter Store fixes all three: auto-published on every change, auto-pulled on every failure, and IAM splits read from write.

The CLAUDE_OAUTH_CREDENTIALS entry has been removed from secrets.json as of 2026-05-19. Anything that previously referenced it should call ~/apps/cc/recipes/claude-creds-pull.sh instead.

  • 2026-05-08 — keychain no-timeout setting documented. Not applied. Predicted not to be durable; was right.
  • 2026-05-11no-timeout applied on Studio. Re-locked within hours via lock-on-sleep.
  • 2026-05-13 — file-based ~/.claude/.credentials.json approach + one-time snapshot to secrets.json. No automation tied it together.
  • 2026-05-19 — same problem returns. Both Studio and MBP missing the file; secrets.json snapshot returns 401 (refresh token rotated, no one re-snapshotted). Three “durable” fixes regressed in 11 days because each was documented but not automated.
  • 2026-05-19 — Parameter Store + launchd auto-publish + wrapper auto-recover lands. End-to-end test on MBP passes: delete .credentials.json, run claude -p, returns clean output in 12.88 seconds with no human action.