From 5c9994a596f6ff46f716dda1942774f8ee1ea070 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 15:52:35 -0700 Subject: [PATCH] fix(pair-agent): keep the ngrok authtoken out of the transcript and shell argv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The not-authed flow told the user to paste their ngrok authtoken into the chat so the agent could run `ngrok config add-authtoken` — putting a live credential in the transcript, tool-call argv, and anything the transcript syncs to. The user now runs the auth command in their own terminal; the agent only verifies via `ngrok config check`, and a pasted token triggers a rotate-and-reauth instruction. A static test pins that no agent-run bash fence ever contains add-authtoken again. Fixes #2335. Co-Authored-By: Claude Fable 5 --- pair-agent/SKILL.md | 25 +++++++++---- pair-agent/SKILL.md.tmpl | 25 +++++++++---- test/pair-agent-token-hygiene.test.ts | 54 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 test/pair-agent-token-hygiene.test.ts diff --git a/pair-agent/SKILL.md b/pair-agent/SKILL.md index 19218eba8..8b3589d0e 100644 --- a/pair-agent/SKILL.md +++ b/pair-agent/SKILL.md @@ -960,23 +960,34 @@ Then tell the user: "Copy the block above and paste it into your other agent's chat. The setup key expires in 5 minutes." -**If ngrok is installed but NOT authed:** Walk the user through authentication: +**If ngrok is installed but NOT authed:** Walk the user through authentication. + +SECURITY: the ngrok authtoken must NEVER pass through this chat, a Bash tool +call, or shell history — a token pasted here lands in the transcript (and +anything the transcript syncs to). The user runs the auth command in their +OWN terminal; you only verify the result. Tell the user: -"ngrok is installed but not logged in. Let's fix that: +"ngrok is installed but not logged in. Let's fix that — in your own terminal +(not here; the token should never enter this chat): 1. Go to https://dashboard.ngrok.com/get-started/your-authtoken 2. Copy your auth token -3. Come back here and I'll run the auth command for you." +3. In YOUR terminal, run: ngrok config add-authtoken +4. Tell me 'done' when finished." -STOP here and wait for the user to provide their auth token. +STOP here and wait for the user to say they've run it. Do NOT accept a pasted +token; if the user pastes one anyway, tell them to rotate it at +https://dashboard.ngrok.com (it's now in the transcript) and re-auth in their +terminal with the new one. -When they provide it, run: +When they say done, verify without touching the token: ```bash -ngrok config add-authtoken THEIR_TOKEN +ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED" ``` -Then retry `$B pair-agent --client TARGET_HOST`. +If `NGROK_AUTHED`: retry `$B pair-agent --client TARGET_HOST`. +If still `NGROK_NOT_AUTHED`: ask them to re-run the command in their terminal. **If ngrok is NOT installed:** Walk the user through installation: diff --git a/pair-agent/SKILL.md.tmpl b/pair-agent/SKILL.md.tmpl index 75ed42d59..1dc89f20d 100644 --- a/pair-agent/SKILL.md.tmpl +++ b/pair-agent/SKILL.md.tmpl @@ -155,23 +155,34 @@ Then tell the user: "Copy the block above and paste it into your other agent's chat. The setup key expires in 5 minutes." -**If ngrok is installed but NOT authed:** Walk the user through authentication: +**If ngrok is installed but NOT authed:** Walk the user through authentication. + +SECURITY: the ngrok authtoken must NEVER pass through this chat, a Bash tool +call, or shell history — a token pasted here lands in the transcript (and +anything the transcript syncs to). The user runs the auth command in their +OWN terminal; you only verify the result. Tell the user: -"ngrok is installed but not logged in. Let's fix that: +"ngrok is installed but not logged in. Let's fix that — in your own terminal +(not here; the token should never enter this chat): 1. Go to https://dashboard.ngrok.com/get-started/your-authtoken 2. Copy your auth token -3. Come back here and I'll run the auth command for you." +3. In YOUR terminal, run: ngrok config add-authtoken +4. Tell me 'done' when finished." -STOP here and wait for the user to provide their auth token. +STOP here and wait for the user to say they've run it. Do NOT accept a pasted +token; if the user pastes one anyway, tell them to rotate it at +https://dashboard.ngrok.com (it's now in the transcript) and re-auth in their +terminal with the new one. -When they provide it, run: +When they say done, verify without touching the token: ```bash -ngrok config add-authtoken THEIR_TOKEN +ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED" ``` -Then retry `$B pair-agent --client TARGET_HOST`. +If `NGROK_AUTHED`: retry `$B pair-agent --client TARGET_HOST`. +If still `NGROK_NOT_AUTHED`: ask them to re-run the command in their terminal. **If ngrok is NOT installed:** Walk the user through installation: diff --git a/test/pair-agent-token-hygiene.test.ts b/test/pair-agent-token-hygiene.test.ts new file mode 100644 index 000000000..dd1ec7edf --- /dev/null +++ b/test/pair-agent-token-hygiene.test.ts @@ -0,0 +1,54 @@ +import { describe, test, expect } from 'bun:test'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Token hygiene for /pair-agent (#2335): the ngrok authtoken must never walk +// through the chat transcript or a Bash tool call. The skill may INSTRUCT the +// user to run `ngrok config add-authtoken` in their own terminal, but no +// agent-executed bash fence may contain the command — an agent-run +// `ngrok config add-authtoken THEIR_TOKEN` means the token arrived via the +// transcript and landed in shell argv/history. + +const ROOT = path.resolve(import.meta.dir, '..'); + +function fencedBashBlocks(markdown: string): string[] { + // Line-based fence walk: a naive /```...```/ regex mis-pairs a CLOSING + // fence with the next block's opener and swallows the prose in between. + const blocks: string[] = []; + let inFence = false; + let lang = ''; + let current: string[] = []; + for (const line of markdown.split('\n')) { + if (line.trimStart().startsWith('```')) { + if (!inFence) { + inFence = true; + lang = line.trim().slice(3).trim().toLowerCase(); + current = []; + } else { + inFence = false; + if (lang === '' || lang === 'bash' || lang === 'sh') blocks.push(current.join('\n')); + } + continue; + } + if (inFence) current.push(line); + } + return blocks; +} + +describe('pair-agent ngrok token hygiene (#2335)', () => { + const files = ['pair-agent/SKILL.md', 'pair-agent/SKILL.md.tmpl']; + + test.each(files)('%s: no agent-run bash fence contains add-authtoken', (rel) => { + const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8'); + for (const block of fencedBashBlocks(content)) { + expect(block).not.toContain('add-authtoken'); + } + }); + + test.each(files)('%s: instructs that the token never enters the chat', (rel) => { + const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8'); + expect(content.toLowerCase()).toContain('never enter this chat'); + // The recovery path for an accidentally pasted token must exist. + expect(content.toLowerCase()).toContain('rotate'); + }); +});