Skip to content

Hook scripts spawn gh/a365/agentsplayground by bare name through the shell #82

Description

scripts/check-version.js and the stop-time check scripts invoke external tools by bare name through a shell, with the session workspace as the current directory. Bare-name resolution through cmd.exe is current-directory-first, so where that leg is active, an executable planted in the opened repository runs instead of the intended tool — and on hosts that disable the cwd leg, behavior silently differs by environment.

Call sites (current main)

plugins/agent365/scripts/check-version.js runs at every session start:

const raw = execSync(
  'gh release view --repo microsoft/agent365-skills --json tagName -q .tagName',
  { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
).toString().trim();

The stop-time check scripts use the same shape via execSync(cmd, { cwd }) with cwd = the workspace:

  • hooks/stop/validate-a365-setup.js:39 — runCmd('a365 --version')
  • hooks/stop/validate-make-a365-agent.js:32 — runCmd('a365 --version')
  • hooks/stop/validate-add-workiq-tools.js:377,379 — runCmd('a365 --version'), runCmd('a365 develop list-configured')
  • hooks/stop/validate-test-local.js:56 — run('agentsplayground --version')

(scripts/install.js also spawns gh bare, but it is user-invoked, so it is lower priority.)

Why this is the wrong shape

On Windows, Node's execSync spawns cmd.exe /d /s /c, and cmd.exe resolves a bare command name from the current directory first, then PATH. These hooks run with cwd = the repository the user opened. Two consequences:

  1. Workspace-planted executables run wherever the cwd leg is active. A gh.cmd / gh.exe / gh.bat at the root of the opened repository executes in place of the installed gh — at session start, before any user interaction. Its stdout also becomes plugin-visible text: check-version.js prints whatever gh returns as the "update available" version, with advice pointing at ./scripts/refresh.ps1. Reproduced by placing a gh.cmd that echoes a chosen version string into an empty workspace and running node scripts/check-version.js from that directory: the planted binary executes and the plugin renders its output as the update warning.
  2. Behavior is host-dependent. Some hosts set NoDefaultCurrentDirectoryInExePath in hook environments (Claude Code does), which disables cmd.exe's current-directory lookup — the call then resolves from PATH only. So the same shipped code resolves names differently depending on the host and entry point (interactive session, manual node scripts/check-version.js, CI, other harnesses).

The a365 and agentsplayground call sites have the same property on the stop side (a365.cmd, agentsplayground.cmd in the workspace).

Suggested fix

Spawn an explicitly resolved executable without a shell:

const { execFileSync } = require('child_process');
// resolve gh against PATH only (walk process.env.PATH + the standard
// extensions; fail loudly if absent), then:
execFileSync(ghPath,
  ['release', 'view', '--repo', 'microsoft/agent365-skills', '--json', 'tagName', '-q', '.tagName'],
  { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });

A PATH-only resolver makes behavior identical across hosts and removes the current-directory leg everywhere, guarded or not. The same change applies to the a365 / agentsplayground sites and to install.js's spawnSync('gh', ...).

Related

#81 covers the frontmatter event-key casing that currently keeps these hooks from registering at all; the defects above apply to every execution path of these scripts regardless — the session-start script, the stop checks once they register, and any manual/CI invocation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions