From a9b1d0f7d04db24c80216bdf7c41037faf21d104 Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Thu, 13 Aug 2026 14:14:31 -0400 Subject: [PATCH 1/7] fix: authenticate the demo box's git pull with the job's own GITHUB_TOKEN Every deploy-demo-box run since 2026-08-13 has failed at the "Pull latest main" step with `fatal: could not read Username for 'https://github.com': No such device or address` (exit 128), so main was never actually reaching the box: the stack kept serving whatever was already deployed. The box's persistent clone at /home/sakib/hive authenticated its HTTPS remote from some credential external to this workflow. That credential worked through the 2026-08-12 20:04 UTC run (its own log shows a real fetch, not an error) and was gone by the next deploy 17 hours later, with the workflow file byte-identical across that gap. That is an expired or rotated credential, not a missing config line. Fix: stop depending on any credential stored on the box. The Pull latest main step now authenticates with this job's own ephemeral GITHUB_TOKEN via `git -c http.extraheader`, scoped to the single fetch/pull invocation and never written to disk, the same mechanism actions/checkout uses internally. Nothing here needs future rotation. GIT_TERMINAL_PROMPT=0 plus an explicit ::error:: on a failed fetch makes a bad credential fail loud and specific instead of the opaque prompt error this outage produced. actions/checkout replacing the box's persistent clone entirely was considered and rejected: every later step in this job assumes /home/sakib/hive holds the box's own untracked .env and Docker build cache, and actions/checkout's default `clean: true` would delete that .env on first use. Grepped the repo for other automated pulls on the box: none. scripts/ install.sh has a similar git fetch/checkout pair, but it targets /opt/hive for a human-run, interactive Enterprise install, not an unattended CI loop, so it is out of scope here. Buglog entry (for the follow-up buglog-only PR against main): {"error_message": "deploy-demo-box \"Pull latest main\" step: fatal: could not read Username for 'https://github.com': No such device or address (exit 128)", "root_cause": "the demo box's persistent git clone at /home/sakib/hive authenticated its HTTPS remote from a credential external to the workflow (stored PAT or credential helper on the box); that credential worked through the 2026-08-12 20:04 UTC deploy and was gone by the next deploy 17 hours later with no workflow change in between, so it expired or was rotated out from under an unattended git pull with no fallback", "fix": "authenticate the box's own fetch/pull with the deploy job's ephemeral GITHUB_TOKEN via git -c http.extraheader, scoped per-invocation and never persisted to disk, plus GIT_TERMINAL_PROMPT=0 and an explicit ::error:: so a bad credential fails loud instead of an opaque prompt error", "tags": ["ci", "deploy-demo-box", "git", "credentials", "self-hosted-runner"]} --- .github/workflows/deploy-demo-box.yml | 58 +++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index 55b69b60d..ba3abdfb1 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -211,18 +211,70 @@ jobs: # when this job is skipped. actions:read exists for one reason: the # agent-engine step below downloads the CI-built Apptainer image when the # box has none, since that image is linux/amd64 only and cannot be built - # on the dev machine. + # on the dev machine. contents:read is for the Pull latest main step + # below: it authenticates the box's own HTTPS remote with this job's own + # ephemeral github.token instead of a credential stored on the box. permissions: actions: read + contents: read timeout-minutes: 15 steps: - name: Pull latest main + # Root cause of the 2026-08-13 outage (every deploy since failing + # `fatal: could not read Username for 'https://github.com': No such + # device or address`, exit 128): the box's persistent clone at + # /home/sakib/hive authenticates its HTTPS remote from some + # credential external to this workflow (a stored PAT or a git + # credential helper on the box; this runner has no shell access to + # inspect which). That credential worked through the 2026-08-12 + # 20:04 UTC run (confirmed from that run's own log: `git fetch` + # printed a real ref listing, not an error) and was gone by the next + # deploy 17 hours later, with the workflow file byte-identical + # across that gap (`git log -- .github/workflows/deploy-demo-box.yml` + # between those two commits is empty). That is an expired or rotated + # credential, not a missing config line: nothing in this repo + # changed to cause it. + # + # Fix: stop depending on any credential that lives on the box at + # all. `github.token` is this job's own ephemeral, auto-rotated + # GITHUB_TOKEN (scoped read-only by `contents: read` above, minted + # fresh per run, never stored on disk), the same mechanism + # actions/checkout uses internally. It is supplied only as a `git + # -c http.extraheader` value scoped to this one invocation, never + # written to the repo's or the box's persistent git config, so + # there is nothing here to expire or rotate again. GitHub + # automatically masks the token if it ever appeared in a log line; + # it does not, since it is only interpolated into a shell variable. + # + # actions/checkout itself was considered instead of fixing the pull + # in place, letting the runner's own already-authenticated checkout + # do the fetch. Rejected: every step below assumes /home/sakib/hive is + # a long-lived directory carrying the box's own untracked .env + # (every secret this stack runs on) and Docker build cache. + # actions/checkout's default `clean: true` runs `git clean -ffdx`, + # which would delete that untracked .env on first use, a bigger + # and riskier change than fixing the credential the persistent + # clone already uses. + # + # GIT_TERMINAL_PROMPT=0 makes a still-bad credential fail in one + # git-native line instead of the opaque "could not read Username / + # No such device or address" this outage actually produced (that + # message is itself just what a denied non-interactive prompt looks + # like on a runner with no tty). + env: + GIT_TERMINAL_PROMPT: "0" run: | set -euo pipefail cd /home/sakib/hive git checkout main - git fetch origin - git pull --ff-only origin main + auth_header="AUTHORIZATION: basic $(printf 'x-access-token:%s' '${{ github.token }}' | base64 -w0)" + if ! git -c "http.https://github.com/.extraheader=${auth_header}" \ + fetch origin main; then + echo "::error::git fetch failed authenticating to https://github.com with this job's own GITHUB_TOKEN (contents: read). This is a credential/permission failure, not a missing branch or ref: check the deploy job's permissions block and that the box's git remote is still https://github.com/${{ github.repository }}.git (git -C /home/sakib/hive remote -v)." >&2 + exit 1 + fi + git -c "http.https://github.com/.extraheader=${auth_header}" \ + pull --ff-only origin main - name: Install and restart the agent-engine launch daemon # Issue #780. Cowork tasks need an Apptainer sandbox per task, and From 28ffb0e5ae1d85f9c8790aba141093cc8b0e386e Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Thu, 13 Aug 2026 14:17:49 -0400 Subject: [PATCH 2/7] fix: stop asserting a specific cause in the fetch failure message CodeRabbit review on PR #898: the previous ::error:: text asserted the fetch failure is a credential/permission problem specifically, which would mislead debugging if a future failure is actually a missing ref, DNS, TLS, or remote-URL issue instead. Point at the preceding git error output as the real diagnostic source and keep the same two most likely places to check, without asserting the cause. --- .github/workflows/deploy-demo-box.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index ba3abdfb1..f0d84f548 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -270,7 +270,7 @@ jobs: auth_header="AUTHORIZATION: basic $(printf 'x-access-token:%s' '${{ github.token }}' | base64 -w0)" if ! git -c "http.https://github.com/.extraheader=${auth_header}" \ fetch origin main; then - echo "::error::git fetch failed authenticating to https://github.com with this job's own GITHUB_TOKEN (contents: read). This is a credential/permission failure, not a missing branch or ref: check the deploy job's permissions block and that the box's git remote is still https://github.com/${{ github.repository }}.git (git -C /home/sakib/hive remote -v)." >&2 + echo "::error::git fetch origin main failed. See the git error above for the actual cause. If it names the username/credential, check the deploy job's permissions block (needs contents: read) and that the box's git remote is still https://github.com/${{ github.repository }}.git (git -C /home/sakib/hive remote -v)." >&2 exit 1 fi git -c "http.https://github.com/.extraheader=${auth_header}" \ From f41731a0ed009819353df1298a4b131e17434427 Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Fri, 14 Aug 2026 03:16:40 -0400 Subject: [PATCH 3/7] fix: guard against a repointed remote and log the deployed SHA Adversarial review on PR #898 found the extraheader token authenticates any public github.com repo, not just this one, so a repointed origin on the box would pull the wrong source while still reporting success. Check the remote URL before using the token, and fail loud if it does not match this repository. Also add an explicit ::error:: on the pull-failure path (only the fetch had one) and log the resulting commit SHA after a successful pull, so a future silent-stale case is visible in the run log rather than requiring a read of raw git output. --- .github/workflows/deploy-demo-box.yml | 30 ++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index f0d84f548..21148ad72 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -267,14 +267,38 @@ jobs: set -euo pipefail cd /home/sakib/hive git checkout main + + # The extraheader below authenticates ANY public github.com repo, + # not just this one, so it would fetch happily from a repointed + # origin. Check first, or a repointed remote pulls the wrong + # content while reporting the same green this whole fix exists to + # make honest. + expected_url="https://github.com/${{ github.repository }}" + actual_url=$(git remote get-url origin) + case "$actual_url" in + "$expected_url"|"$expected_url.git") ;; + *) + echo "::error::box remote 'origin' is $actual_url, expected $expected_url(.git). Refusing to pull: this job's GITHUB_TOKEN would authenticate a fetch from any public github.com repo, so a repointed remote would silently deploy the wrong source. Fix with: git -C /home/sakib/hive remote set-url origin $expected_url.git" >&2 + exit 1 + ;; + esac + auth_header="AUTHORIZATION: basic $(printf 'x-access-token:%s' '${{ github.token }}' | base64 -w0)" if ! git -c "http.https://github.com/.extraheader=${auth_header}" \ fetch origin main; then - echo "::error::git fetch origin main failed. See the git error above for the actual cause. If it names the username/credential, check the deploy job's permissions block (needs contents: read) and that the box's git remote is still https://github.com/${{ github.repository }}.git (git -C /home/sakib/hive remote -v)." >&2 + echo "::error::git fetch origin main failed. See the git error above for the actual cause. If it names the username/credential, check the deploy job's permissions block (needs contents: read)." >&2 + exit 1 + fi + if ! git -c "http.https://github.com/.extraheader=${auth_header}" \ + pull --ff-only origin main; then + echo "::error::git pull --ff-only origin main failed after a successful fetch, so this is a fast-forward divergence (a stray local commit on the box, or main force-pushed), not a credential problem. See the git error above; git -C /home/sakib/hive log --oneline -5 origin/main on the box will show what diverged." >&2 exit 1 fi - git -c "http.https://github.com/.extraheader=${auth_header}" \ - pull --ff-only origin main + # --ff-only means this line only runs after a real advance or a + # no-op "Already up to date.", never after a silent partial pull, + # but nothing upstream of this could tell the two apart without + # reading raw git output. Record the outcome explicitly instead. + echo "deployed commit: $(git rev-parse HEAD)" - name: Install and restart the agent-engine launch daemon # Issue #780. Cowork tasks need an Apptainer sandbox per task, and From f3a0042af33b41f8eb933cc9bc34eec7fe49a3f8 Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Fri, 14 Aug 2026 03:20:46 -0400 Subject: [PATCH 4/7] fix: keep the credential out of argv, mask the derived blob, redact the remote URL Security review on PR #898 found three HIGH severity issues in the previous commit's fix and two MEDIUM ones worth acting on rather than answering with a rebuttal. HIGH, fixed: - The Basic-auth header was passed via `git -c ...` on the command line, world-readable through /proc//cmdline on a stock box for the life of the fetch and pull. Switched to GIT_CONFIG_COUNT/KEY_n/VALUE_n environment variables, readable only by the same uid or root, same effect, no argv exposure. - The base64 Basic-auth blob derived from the token was never masked. GitHub only auto-masks the literal token, not a value derived from it. Added an explicit ::add-mask:: on the blob before it is used, the same step actions/checkout itself takes for the identical value. - The remote-URL guard added in the previous commit printed the box's origin URL unredacted in its error message. If that URL ever embeds a credential, the guard would leak it into the run log while checking for exactly this class of problem. Redacted once, reused everywhere the value is printed. MEDIUM, fixed: - The previous comment claimed the token is never written to disk. False: a `${{ }}` expression inside a run: block materializes into a script under the runner's temp directory, and this runner is not ephemeral. The token now reaches the script only via the GH_TOKEN env var, never interpolated into the script text. - Nothing reset credential.helper, so a green run did not actually prove the new mechanism authenticated rather than a leftover on-box credential. GIT_CONFIG_KEY_1/VALUE_1 empties it for this invocation. Also added three secret-free diagnostic lines (credential.helper state, redacted remote URL, presence of ~/.git-credentials) so the next outage does not start from the same unknowns this PR's own Verification section flagged as unprovable without box access. - pull --ff-only moves the ref and writes the worktree as two separate steps. A run killed mid-pull (this job's own timeout, a cancel, or a box reboot) can leave HEAD advanced over a half-written tree that the next run reports as current. Added a tracked-file dirty check after the pull, plus a leading `rm -f .git/index.lock` for the matching stale-lock case (safe: the workflow-level concurrency group serializes this job). --- .github/workflows/deploy-demo-box.yml | 87 ++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index 21148ad72..fbdd9bf10 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -238,13 +238,20 @@ jobs: # Fix: stop depending on any credential that lives on the box at # all. `github.token` is this job's own ephemeral, auto-rotated # GITHUB_TOKEN (scoped read-only by `contents: read` above, minted - # fresh per run, never stored on disk), the same mechanism - # actions/checkout uses internally. It is supplied only as a `git - # -c http.extraheader` value scoped to this one invocation, never - # written to the repo's or the box's persistent git config, so - # there is nothing here to expire or rotate again. GitHub - # automatically masks the token if it ever appeared in a log line; - # it does not, since it is only interpolated into a shell variable. + # fresh per run), the same mechanism actions/checkout uses + # internally. It is supplied as per-invocation `GIT_CONFIG_*` + # environment variables rather than `.git/config`, so there is + # nothing here to expire or rotate again. Two things a first pass + # of this comment got wrong, corrected after security review on + # this PR: the interpolated value IS written to disk, in the + # runner's own materialized step script under $RUNNER_TEMP (this + # runner is not ephemeral, see the box-reboot note below, so that + # file is not on a fresh tmpfs); and GitHub only auto-masks the + # literal token, not the base64 Basic-auth blob derived from it, so + # that blob is masked explicitly below instead. The token itself + # now reaches the script only via the `GH_TOKEN` env var, never + # interpolated into the script text, matching the pattern the + # agent-engine step below already uses. # # actions/checkout itself was considered instead of fixing the pull # in place, letting the runner's own already-authenticated checkout @@ -263,37 +270,87 @@ jobs: # like on a runner with no tty). env: GIT_TERMINAL_PROMPT: "0" + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail cd /home/sakib/hive + + # A leftover .git/index.lock from a prior run killed mid checkout + # (the timeout-minutes below, a manual cancel, or a box reboot + # this file's own header says the runner does not survive) would + # fail this line with a lock message instead of the real problem. + # Safe to clear unconditionally: the workflow-level concurrency + # group (deploy-demo-box, cancel-in-progress: false) guarantees no + # other run of this job is genuinely holding it right now. + rm -f .git/index.lock git checkout main - # The extraheader below authenticates ANY public github.com repo, - # not just this one, so it would fetch happily from a repointed + # The token below authenticates ANY public github.com repo, not + # just this one, so it would fetch happily from a repointed # origin. Check first, or a repointed remote pulls the wrong # content while reporting the same green this whole fix exists to # make honest. + # Redacted unconditionally, not just when a mismatch is found: + # origin can embed a credential (https://user:token@github.com/...) + # and this value reaches both the error branch below and the + # diagnostic dump after it, so redacting once here keeps every + # later use safe by construction instead of relying on each print + # site to remember. expected_url="https://github.com/${{ github.repository }}" actual_url=$(git remote get-url origin) + redacted_url=$(printf '%s' "$actual_url" | sed -E 's#//[^/@]*@#//REDACTED@#') case "$actual_url" in "$expected_url"|"$expected_url.git") ;; *) - echo "::error::box remote 'origin' is $actual_url, expected $expected_url(.git). Refusing to pull: this job's GITHUB_TOKEN would authenticate a fetch from any public github.com repo, so a repointed remote would silently deploy the wrong source. Fix with: git -C /home/sakib/hive remote set-url origin $expected_url.git" >&2 + echo "::error::box remote 'origin' is $redacted_url, expected $expected_url(.git). Refusing to pull: this job's GITHUB_TOKEN would authenticate a fetch from any public github.com repo, so a repointed remote would silently deploy the wrong source. Fix with: git -C /home/sakib/hive remote set-url origin $expected_url.git" >&2 exit 1 ;; esac - auth_header="AUTHORIZATION: basic $(printf 'x-access-token:%s' '${{ github.token }}' | base64 -w0)" - if ! git -c "http.https://github.com/.extraheader=${auth_header}" \ - fetch origin main; then + # Secret-free, and answers exactly what this PR could not verify + # without box access: whether a credential path older than this + # fix could still shadow or rescue it. + git config --show-origin --get-all credential.helper || echo "no credential helper configured" + echo "origin: $redacted_url" + [ -f "$HOME/.git-credentials" ] && echo "a stored credential file exists on the box" || true + + # GIT_CONFIG_* env vars, not `git -c` on argv: argv is world + # readable via /proc//cmdline on a stock box with no + # hidepid, env is readable only by the same uid or root. Slot 1 + # empties credential.helper for this invocation so a stored PAT + # or netrc on the box cannot shadow or rescue the header in slot + # 0: a failure below is unambiguously this token's failure. + b64=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w0) + echo "::add-mask::$b64" + export GIT_CONFIG_COUNT=2 + export GIT_CONFIG_KEY_0="http.https://github.com/.extraheader" + export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $b64" + export GIT_CONFIG_KEY_1="credential.helper" + export GIT_CONFIG_VALUE_1="" + + if ! git fetch origin main; then echo "::error::git fetch origin main failed. See the git error above for the actual cause. If it names the username/credential, check the deploy job's permissions block (needs contents: read)." >&2 exit 1 fi - if ! git -c "http.https://github.com/.extraheader=${auth_header}" \ - pull --ff-only origin main; then + if ! git pull --ff-only origin main; then echo "::error::git pull --ff-only origin main failed after a successful fetch, so this is a fast-forward divergence (a stray local commit on the box, or main force-pushed), not a credential problem. See the git error above; git -C /home/sakib/hive log --oneline -5 origin/main on the box will show what diverged." >&2 exit 1 fi + + # pull --ff-only moves the ref and writes the worktree as two + # separate steps, not one atomic operation. A run killed in + # between (this job's own timeout, a cancel, or a box reboot) + # leaves HEAD advanced over a half-written tree; the next run's + # fetch/pull then reports "Already up to date" over that same + # half-written tree and goes green. This catches either case: + # this run's own interruption never reaches this line anyway, so + # only a genuinely dirty tree (this run's or a leftover one) fails + # it. + if [ -n "$(git status --porcelain -uno)" ]; then + echo "::error::working tree at /home/sakib/hive has tracked-file changes against HEAD after the pull, consistent with a previous deploy interrupted mid checkout. Recover with: git -C /home/sakib/hive reset --hard origin/main (safe: reset --hard does not remove untracked files, so the box's .env is not touched)." >&2 + exit 1 + fi + # --ff-only means this line only runs after a real advance or a # no-op "Already up to date.", never after a silent partial pull, # but nothing upstream of this could tell the two apart without From d23b8c5338bf8a1c5544741823ce171edc19579b Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Fri, 14 Aug 2026 03:22:18 -0400 Subject: [PATCH 5/7] docs: note the widened GH_TOKEN scope on the agent-engine step Security review on PR #898 pointed out that contents:read, added for the Pull latest main fix, also widens the job-scoped GH_TOKEN this later step already used at only actions:read. Correct and unavoidable at job-level permissions granularity, just undocumented until now. Also links issue #902, filed for the separate, pre-existing question of who can trigger this job via workflow_dispatch. --- .github/workflows/deploy-demo-box.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index fbdd9bf10..c229e1317 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -363,6 +363,13 @@ jobs: # therefore runs on the box as this unprivileged user and # control-plane talks to it over a Unix socket; see the script header # for why the container is deliberately NOT given that privilege. + # GH_TOKEN below is job-scoped, so the contents:read this job's + # permissions block grants for the Pull latest main step above also + # reaches this GH_TOKEN, which previously only carried the default + # actions:read scope. Correct and unavoidable at job-level + # permissions granularity; noted since it was not called out when + # contents:read was added. See issue #902 for the separate, + # pre-existing question of who can trigger this job at all. working-directory: /home/sakib/hive env: GH_TOKEN: ${{ github.token }} From ae2c4597bb3faf7d2f2bcec9b4e43a169017a075 Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Fri, 14 Aug 2026 12:00:57 -0400 Subject: [PATCH 6/7] fix: stop printing credential.helper's value, age-gate the lock removal Security review, second pass on PR #898, confirmed the three HIGH argv/ mask/redaction fixes hold in the actual code, then found three more: MEDIUM: `git config --show-origin --get-all credential.helper` prints the helper's value, and a helper can be an inline shell snippet with a PAT embedded, exactly the on-box credential this PR suspects. Switched to `--name-only --get-regexp`, which reports that a helper is configured without printing what it contains. MEDIUM: the unconditional `rm -f .git/index.lock` could stomp a lock held by a genuinely running process on this shared checkout (git gc --auto, a human on the box), since the workflow-level concurrency group only serializes this workflow's own runs against each other, not every writer of this clone. Age-gated to `find -mmin +15 -delete`, matching this job's own timeout-minutes. LOW: the redaction regex stopped at the first @, which would leave part of a credential exposed if the secret itself contained an unencoded @. Made greedy to match the last @ instead. Also added a `.netrc` presence check alongside the existing `.git-credentials` one, and an explicit ::error:: on an empty GH_TOKEN instead of silently building a useless auth header from it. --- .github/workflows/deploy-demo-box.yml | 31 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index c229e1317..9c15e2963 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -279,10 +279,13 @@ jobs: # (the timeout-minutes below, a manual cancel, or a box reboot # this file's own header says the runner does not survive) would # fail this line with a lock message instead of the real problem. - # Safe to clear unconditionally: the workflow-level concurrency - # group (deploy-demo-box, cancel-in-progress: false) guarantees no - # other run of this job is genuinely holding it right now. - rm -f .git/index.lock + # Age-gated, not unconditional: this clone is the box's shared + # checkout, other writers (git gc --auto, a human on the box) can + # legitimately hold this lock, and the workflow-level concurrency + # group only serializes this workflow's own runs against each + # other. 15 minutes matches this job's own timeout-minutes below, + # so a lock younger than that is still someone's live operation. + find .git -maxdepth 1 -name index.lock -mmin +15 -delete git checkout main # The token below authenticates ANY public github.com repo, not @@ -298,7 +301,10 @@ jobs: # site to remember. expected_url="https://github.com/${{ github.repository }}" actual_url=$(git remote get-url origin) - redacted_url=$(printf '%s' "$actual_url" | sed -E 's#//[^/@]*@#//REDACTED@#') + # Greedy up to the LAST @, not the first: a credential whose + # secret half itself contains an unencoded @ would otherwise + # leave part of it printed after a first-@ match. + redacted_url=$(printf '%s' "$actual_url" | sed -E 's#//.*@#//REDACTED@#') case "$actual_url" in "$expected_url"|"$expected_url.git") ;; *) @@ -309,10 +315,21 @@ jobs: # Secret-free, and answers exactly what this PR could not verify # without box access: whether a credential path older than this - # fix could still shadow or rescue it. - git config --show-origin --get-all credential.helper || echo "no credential helper configured" + # fix could still shadow or rescue it. --name-only reports THAT a + # helper is configured, never its value: a helper can be an + # inline shell snippet with a PAT embedded in it, and that value + # is exactly the on-box credential this whole PR suspects. + git config --name-only --get-regexp '^credential\.helper$' \ + && echo "credential.helper is configured (value withheld)" \ + || echo "no credential helper configured" echo "origin: $redacted_url" [ -f "$HOME/.git-credentials" ] && echo "a stored credential file exists on the box" || true + [ -f "$HOME/.netrc" ] && echo "a .netrc file exists on the box" || true + + if [ -z "${GH_TOKEN:-}" ]; then + echo "::error::GH_TOKEN is empty. This job's permissions block (needs contents: read) or the runner's token minting is the problem, not the box." >&2 + exit 1 + fi # GIT_CONFIG_* env vars, not `git -c` on argv: argv is world # readable via /proc//cmdline on a stock box with no From 4d6ee25a37c14176493df2038b0590e064b7e5fb Mon Sep 17 00:00:00 2001 From: Sakib Sadman Shajib Date: Fri, 14 Aug 2026 12:01:45 -0400 Subject: [PATCH 7/7] fix: keep --show-origin on the credential.helper diagnostic Security review pointed out --name-only alone drops the config file origin that made this diagnostic useful in the first place. --show-origin combines safely with --name-only (origin path, still no value), so add it back: reports whether a helper is configured AND which file configures it, with nothing printable left. --- .github/workflows/deploy-demo-box.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-demo-box.yml b/.github/workflows/deploy-demo-box.yml index 9c15e2963..4ec3b21e8 100644 --- a/.github/workflows/deploy-demo-box.yml +++ b/.github/workflows/deploy-demo-box.yml @@ -319,7 +319,7 @@ jobs: # helper is configured, never its value: a helper can be an # inline shell snippet with a PAT embedded in it, and that value # is exactly the on-box credential this whole PR suspects. - git config --name-only --get-regexp '^credential\.helper$' \ + git config --show-origin --name-only --get-regexp '^credential\.helper$' \ && echo "credential.helper is configured (value withheld)" \ || echo "no credential helper configured" echo "origin: $redacted_url"