Skip to content

feat(scripts): workspace-misroute guard (fail loud in the wrong checkout) — #679 D3 - #680

Merged
DealAppSeo merged 1 commit into
mainfrom
claude/workspace-guard-2026-11-07
Nov 8, 2026
Merged

DealAppSeo merged 1 commit into
mainfrom
claude/workspace-guard-2026-11-07

Conversation

@DealAppSeo

Copy link
Copy Markdown
Owner

What

A small, dependency-free guard script + test that lets any loop or session assert it is running in the right checkout before doing repo-specific work, and fail loudly if not.

node scripts/check-workspace.cjs                 # expects origin = DealAppSeo/trustshell (exit 0 / 1)
node scripts/check-workspace.cjs owner/repo      # expects that repo
TRUSTSHELL_EXPECT_REPO=owner/repo node scripts/check-workspace.cjs

Why

During the #679 sprint the session launcher bound a TrustShell task to the old ihh-ica project while the working directory was the trustshell-tl worktree — so start_session resolved the wrong repo and work risked landing (or silently failing) against the wrong history. The session-binding itself is app-level config a repo script can't change; this is the durable in-repo assertion so the misroute is caught at task startup instead of discovered later.

How it's safe to review

  • workspaceMatches(remoteUrl, expected) is pure — https/ssh forms, optional .git / trailing slash, case-insensitive. Unit-tested against the ihh-ica misroute, a different repo, and empty/garbage remotes.
  • main(argv, env, run) takes an injected shell, so the test asserts both exit paths (0 on match, 1 on misroute) without depending on the checkout it runs in.
  • No new dependencies. Touches only scripts/, tests/, and one package.json alias (npm run check:workspace). No dist/, no runtime code.

Verification

  • npx jest tests/check-workspace.test.ts → 3/3 pass
  • Live in this checkout: exit 0 (origin = trustshell); with DealAppSeo/ihh-ica expected → exit 1 with the loud stderr message.

Draft — part of the #679 developer-trust sprint (DECISION 3). Independent of the security fixes in #679.

🤖 Generated with Claude Code

…rong checkout (#679 D3)

The session launcher bound a TrustShell task to the old `ihh-ica` project while the
working dir was the `trustshell-tl` worktree, so `start_session` resolved the wrong
repo and work risked landing (or silently failing) against the wrong history. The
session-binding itself is app-level config this script cannot change; this is the
in-repo assertion so any loop or session can gate its own work at startup and fail
LOUDLY instead of silently misrouting:

  node scripts/check-workspace.cjs                 # expects origin = DealAppSeo/trustshell
  node scripts/check-workspace.cjs owner/repo      # expects that repo
  TRUSTSHELL_EXPECT_REPO=owner/repo ...            # or via env

workspaceMatches() is pure (https/ssh, optional .git / trailing slash, case-insensitive);
main() takes an injected shell so the test asserts both exit paths without depending on
the checkout it runs in. Exit 0 on match, 1 (loud stderr) on misroute. No deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
trustshell-landing Ready Ready Preview Nov 7, 2026 8:42am UTC

Request Review

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

🧾 Receipt

1 claim(s) checked against GitHub's own check runs on 4985c43: 1 verified · 0 not checked · 0 failed

Claim Said in Evidence Result
tests: npx jest tests/check-workspace.test.ts → **3/3 pass** PR description step `Unit tests` in check passed and the job's own workflow file at this commit runs a matching command, and every other check on this commit finished green, skipped or neutral. ✅ VERIFIED
What this receipt does not prove
  • A green check proves that check passed on this commit. It does not prove the check tested this change.
  • Evidence is read at the strongest level each check offers: the run: command of the job, taken from the workflow file at this commit; else the step name and that step own result; else the check name alone. The Result column says which. A check name is a label, not evidence.
  • At command level the receipt confirms the job runs a command that normally runs this kind of check. It cannot confirm the command asserted anything: a test script that exits 0 without running a test still reads as tests.
  • A job whose steps cannot be read (a third-party check, or no actions: read permission) is matched by its name alone, and a job whose work happens inside a composite action shows no command.
  • Claims are read from the PR description and the latest commit message by plain patterns. A claim worded unusually is not seen. Text in double quotes, code blocks and block quotes is ignored on purpose; text in single quotes is read, because a single quote is also an apostrophe.
  • A check created after this receipt was written is not counted until the receipt runs again.

Receipts by TrustShell. Read-only: it reads this PR and its check runs, and writes only this comment. No code leaves the repo.

@DealAppSeo
DealAppSeo marked this pull request as ready for review November 8, 2026 04:02
@strix-security

strix-security Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Strix Security Review

To fix before merging (1)

PR summary: Adds an opt-in guard script that checks the checkout's git origin matches an expected `owner/repo` and exits 1 with a loud message otherwise, plus a unit test and an `npm run check:workspace` alias.

Review summary

This PR adds a small, dependency-free workspace guard (scripts/check-workspace.cjs) plus a unit test and an npm run check:workspace alias, so a loop or session can assert its git origin is the expected repo before doing repo-specific work. The script uses execSync only with two hard-coded git commands (no interpolation, no shell:true), and the only external input (argv[2] / TRUSTSHELL_EXPECT_REPO) is compared as a string and never executed, so there is no injection surface. One low-severity issue was confirmed: the misroute failure path prints the raw git remote get-url origin output to stderr, so a remote configured with an embedded access token (e.g. https://x-access-token:ghp_...@github.com/...) is disclosed verbatim into terminal scrollback, CI logs and agent session transcripts — inconsistent with the repo's existing redaction hygiene. A second suspicion, that the unanchored github.com regex lets a non-GitHub mirror/proxy remote pass the guard, was reproduced as a real correctness defect but rejected as a security finding: the remote is operator-controlled, the guard is opt-in (no workflow or hook invokes it), and the impact is self-inflicted misrouting with no trust boundary crossed. Worth noting to the author that the guard is advisory only — nothing enforces it yet — and that the test suite has no case for a non-GitHub host containing github.com in its path.

Ruled out (1)
  • Can a non-GitHub remote (mirror/proxy URL containing `github.com` in its path) pass the workspace guard because the regex is unanchored?
    The bypass is real but the only input is the locally configured `git remote get-url origin`, the script has no CI, hook or runtime caller, and the impact is self-inflicted misrouting with no trust boundary crossed — a correctness defect, not a vulnerability.

Fixed the findings? re-run the review, or tag @strix-security in a PR comment to run a fresh review.

Review 1 on this PR · 4985c43 · requested Nov 8 04:02 UTC when the PR was opened


Reviewed by Strix
Re-run review · Configure security review settings

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment on lines +36 to +39
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Workspace guard prints credential-bearing git remote URL verbatim to stderr

Severity: LOW · CWE-532

The new workspace guard writes the raw output of git remote get-url origin into its failure message, so any credentials embedded in the remote URL are disclosed in full. When a checkout is cloned with a token in the URL — the common https://x-access-token:$TOKEN@github.com/owner/repo.git form used by CI runners and agent sandboxes — and the guard fires on a misroute, the token is emitted to stderr, which is captured in terminal scrollback, CI job logs, and agent session transcripts that are read by more parties than the local git config. The script exists precisely to run at task startup in automated sessions, and the misroute path is the one that fires in misconfigured environments, so the leak lands in exactly the logs that get shared and retained. The repository's other tooling already redacts secrets and strips tokens before logging, so this path is inconsistent with the project's own hygiene standard.

Suggested change
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${redactRemote(remote)}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
Prompt to fix with AI
This is a security vulnerability found during a code review.

Vulnerability: Workspace guard prints credential-bearing git remote URL verbatim to stderr
Severity: LOW
CWE: CWE-532

The new workspace guard writes the raw output of `git remote get-url origin` into its failure message, so any credentials embedded in the remote URL are disclosed in full. When a checkout is cloned with a token in the URL — the common `https://x-access-token:$TOKEN@github.com/owner/repo.git` form used by CI runners and agent sandboxes — and the guard fires on a misroute, the token is emitted to stderr, which is captured in terminal scrollback, CI job logs, and agent session transcripts that are read by more parties than the local git config. The script exists precisely to run at task startup in automated sessions, and the misroute path is the one that fires in misconfigured environments, so the leak lands in exactly the logs that get shared and retained. The repository's other tooling already redacts secrets and strips tokens before logging, so this path is inconsistent with the project's own hygiene standard.

Location: scripts/check-workspace.cjs:36-39
Context: Failure message echoes the raw remote URL, including any embedded credentials
```
// Before:
    process.stderr.write(
      `WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
        `Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
    );
// After:
    process.stderr.write(
      `WORKSPACE MISROUTED: origin is "${redactRemote(remote)}", expected ${expected}. ` +
        `Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
    );
```

Location: scripts/check-workspace.cjs:19-25
Context: Adds the redaction helper used by the failure message
```
// Before:
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
  const m = String(remoteUrl)
    .trim()
    .match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
  return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
// After:
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
  const m = String(remoteUrl)
    .trim()
    .match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
  return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}

/** Strip any credentials embedded in a remote URL before it is written to a log. */
function redactRemote(remoteUrl) {
  const s = String(remoteUrl || '').trim();
  return s ? s.replace(/\/\/[^/@\s]+@/, '//<redacted>@') : '(none)';
}
```

How to fix:
Redact the userinfo component of the remote URL before it is written to stderr, or print only the parsed `owner/repo` (which is all the message needs to be actionable). Apply the same redaction to any other diagnostic that echoes a remote URL, and consider a shared helper so future scripts cannot reintroduce the pattern.

Please fix this vulnerability. If you propose a fix, make it concise and minimal.

React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.

Comment on lines +19 to +25
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the redaction helper used by the failure message

Suggested change
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
/** Strip any credentials embedded in a remote URL before it is written to a log. */
function redactRemote(remoteUrl) {
const s = String(remoteUrl || '').trim();
return s ? s.replace(/\/\/[^/@\s]+@/, '//<redacted>@') : '(none)';
}

@DealAppSeo
DealAppSeo merged commit 39d1365 into main Nov 8, 2026
11 checks passed

This branch was successfully deployed

1 active deployment
Preview — 4985c430 Deployed Nov 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant