October 4, 2026 路 #18
The secret that landed in the log
This release job logs in to a registry. Where does the token leak?
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: login
run: |
echo "token=${{ secrets.NPM_TOKEN }}" > .npmrc
npm publishNext puzzle in UTC
One try. The first answer sticks.
How to fix it
GitHub masks secrets it injects as environment values. Expanding secrets.NPM_TOKEN inside run writes the raw token into the step script and the log. Use an env var.
${{ secrets.NPM_TOKEN }} is replaced when the workflow is prepared, before the shell starts. The rendered script, which is what the log shows, contains the token. Masking catches exact strings GitHub already knows about, and it misses the token when it is base64-encoded, split across echoes, or altered by the script. Treat interpolation of a secret into run as a leak.
The same pattern shows up with curl -H "Authorization: Bearer ${{ secrets.TOKEN }}". The header is in the log the moment a later command runs with set -x.
Fix
- name: login
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
npm publish
GitHub masks NPM_TOKEN because it was passed through env. Prefer a trusted publish action that reads the token itself, and set permissions: contents: read plus id-token: write if the registry supports OIDC.