@@ -33,6 +33,7 @@ import { createClaudeCodeProvider } from "./provider.js";
3333import { claudeCodeSandbox } from "./runs.js" ;
3434import { ClaudeCodeStream } from "./stream.js" ;
3535import { claudeCodeMirrorTools } from "./tools.js" ;
36+ import { claudeCodeAnswer } from "./workspace-run.js" ;
3637
3738/**
3839 * Claude Code mode against a Thread workspace on this machine: guest paths
@@ -127,7 +128,7 @@ exit 1
127128const createGuest = async (
128129 options : {
129130 readonly claude ?: boolean ;
130- /** A Claude Code of this version on PATH instead of the current one. */
131+ /** A Claude Code of this version instead of the current one. */
131132 readonly oldClaude ?: string ;
132133 /** The downloaded installer installs the current Claude Code. */
133134 readonly installerWorks ?: boolean ;
@@ -167,16 +168,29 @@ exec /usr/bin/awk "$@"
167168 ? `#!/bin/sh\necho 'mkdir -p "$HOME/.local/bin" && cp ${ join ( root , "claude-latest" ) } "$HOME/.local/bin/claude"'\n`
168169 : "#!/bin/sh\necho 'echo \"Installation failed: checksum mismatch\" >&2; exit 1'\n" ,
169170 ) ;
170- const installClaude = ( ) => tool ( "claude" , FAKE_CLAUDE ) ;
171+ // dx runs Claude Code from where its installer puts it, never from PATH.
172+ const claudeAt = ( body : string ) =>
173+ mkdir ( join ( home , ".local/bin" ) , { recursive : true } ) . then ( ( ) =>
174+ writeFile ( join ( home , ".local/bin/claude" ) , body , { mode : 0o755 } ) ,
175+ ) ;
176+ const installClaude = ( ) => claudeAt ( FAKE_CLAUDE ) ;
171177 if ( options . oldClaude !== undefined )
172- await tool ( "claude" , OLD_CLAUDE ( options . oldClaude ) ) ;
178+ await claudeAt ( OLD_CLAUDE ( options . oldClaude ) ) ;
173179 else if ( options . claude !== false ) await installClaude ( ) ;
174180 const base = await local ( {
175181 cwd : join ( home , "workspace/repo" ) ,
176182 env : { HOME : home , PATH : `${ bin } :/usr/bin:/bin` } ,
177183 } ) . createSandbox ( { id : "guest" } ) ;
178184 const map = ( value : string ) => value . replaceAll ( GUEST_HOME , home ) ;
179185 const ops : GuestOp [ ] = [ ] ;
186+ /**
187+ * What each command carried to the provider: a provider runs it under
188+ * \`bash -l\`, whose profile sees its text and environment.
189+ */
190+ const commands : Array < {
191+ readonly command : string ;
192+ readonly env : Readonly < Record < string , string > > ;
193+ } > = [ ] ;
180194 /** Runs before each operation; a returned error fails it. */
181195 let before : ( op : GuestOp ) => Error | undefined = ( ) => undefined ;
182196 const track = async < A > (
@@ -193,8 +207,10 @@ exec /usr/bin/awk "$@"
193207 const sandbox : Sandbox = {
194208 cwd : `${ GUEST_HOME } /workspace/repo` ,
195209 resolvePath : ( path ) => path ,
196- exec : ( command , execOptions ) =>
197- track ( "exec" , command , ( ) => base . exec ( map ( command ) , execOptions ) ) ,
210+ exec : ( command , execOptions ) => {
211+ commands . push ( { command, env : execOptions ?. env ?? { } } ) ;
212+ return track ( "exec" , command , ( ) => base . exec ( map ( command ) , execOptions ) ) ;
213+ } ,
198214 readFile : ( path ) => track ( "read" , path , ( ) => base . readFile ( map ( path ) ) ) ,
199215 readFileBuffer : ( path ) =>
200216 track ( "read" , path , ( ) => base . readFileBuffer ( map ( path ) ) ) ,
@@ -208,9 +224,23 @@ exec /usr/bin/awk "$@"
208224 rm : ( path , rmOptions ) =>
209225 track ( "write" , path , ( ) => base . rm ( map ( path ) , rmOptions ) ) ,
210226 } ;
227+ /** Files under the guest's home, other than the fake CLI's, that hold it. */
228+ const filesHolding = async ( value : string ) => {
229+ const found : string [ ] = [ ] ;
230+ for ( const name of await readdir ( home , { recursive : true } ) ) {
231+ if ( name === "claude-token" ) continue ;
232+ const contents = await readFile ( join ( home , name ) , "latin1" ) . catch (
233+ ( ) => undefined ,
234+ ) ;
235+ if ( contents ?. includes ( value ) === true ) found . push ( name ) ;
236+ }
237+ return found ;
238+ } ;
211239 return {
212240 sandbox,
213241 ops,
242+ commands,
243+ filesHolding,
214244 installClaude,
215245 /** Put a command on the guest's PATH ahead of the machine's. */
216246 tool,
@@ -486,7 +516,7 @@ describe("Claude Code mode in a workspace", { timeout: 30_000 }, () => {
486516 expect ( await guest . sandbox . exists ( userInstructions ) ) . toBe ( false ) ;
487517 } ) ;
488518
489- it ( "hands the plan token to the CLI alone, never to a process environment" , async ( ) => {
519+ it ( "hands the plan token to the CLI alone, never to a process environment, a command, or a file left behind " , async ( ) => {
490520 const guest = await createGuest ( ) ;
491521 await guest . script ( [
492522 init ( ) ,
@@ -501,6 +531,42 @@ describe("Claude Code mode in a workspace", { timeout: 30_000 }, () => {
501531 expect ( done ) . toMatchObject ( { type : "done" , reason : "stop" } ) ;
502532 expect ( guest . home ( "claude-token" ) ) . toBe ( claude . token ) ;
503533 expect ( guest . home ( "claude-token-readers" ) ) . toBe ( "" ) ;
534+ expect ( guest . commands . length ) . toBeGreaterThan ( 0 ) ;
535+ expect (
536+ guest . commands . filter (
537+ ( { command, env } ) =>
538+ command . includes ( claude . token ) ||
539+ Object . values ( env ) . some ( ( value ) => value . includes ( claude . token ) ) ,
540+ ) ,
541+ ) . toEqual ( [ ] ) ;
542+ expect ( await guest . filesHolding ( claude . token ) ) . toEqual ( [ ] ) ;
543+ } ) ;
544+
545+ it ( "answers a title on the plan token without giving it to a command or leaving it in a file" , async ( ) => {
546+ const guest = await createGuest ( ) ;
547+ await guest . script ( [
548+ init ( ) ,
549+ ...response ( "m1" , [ { text : "Greeting" } ] ) ,
550+ result ( "Greeting" ) ,
551+ ] ) ;
552+ const token = `plan-token-${ crypto . randomUUID ( ) } ` ;
553+ const answer = await claudeCodeAnswer ( guest . sandbox , {
554+ model : "claude-haiku-5-5" ,
555+ token,
556+ input : "Title this: say hello" ,
557+ } ) ;
558+ expect ( answer . text ) . toBe ( "Greeting" ) ;
559+ expect ( guest . home ( "claude-token" ) ) . toBe ( token ) ;
560+ expect ( guest . home ( "claude-token-readers" ) ) . toBe ( "" ) ;
561+ expect ( guest . home ( "claude-stdin.jsonl" ) ) . toBe ( "Title this: say hello" ) ;
562+ expect (
563+ guest . commands . filter (
564+ ( { command, env } ) =>
565+ command . includes ( token ) ||
566+ Object . values ( env ) . some ( ( value ) => value . includes ( token ) ) ,
567+ ) ,
568+ ) . toEqual ( [ ] ) ;
569+ expect ( await guest . filesHolding ( token ) ) . toEqual ( [ ] ) ;
504570 } ) ;
505571
506572 it ( "launches with a prompt too large for one command line" , async ( ) => {
0 commit comments