Summary
The compiled workflow for engine: opencode emits two adjacent steps that contradict each other. The install step skips npm lifecycle scripts; the verify step immediately requires the binary that only the skipped postinstall provides.
This is masked today by the default version pin. gh-aw pins opencode-ai@1.2.14, which predates the point at which that package's postinstall became load-bearing, so smoke-opencode passes. Any user who sets engine.version to 1.15.1 or later -- including the current latest, 1.16.2 -- gets a guaranteed hard failure before the agent container starts.
The generated steps
From .github/workflows/smoke-opencode.lock.yml on main (lines 580-584, and again at 1608-1612 for the non-rootless job):
- name: Install OpenCode
run: npm install --ignore-scripts -g opencode-ai@1.2.14
env:
NPM_CONFIG_MIN_RELEASE_AGE: '3'
- name: Verify OpenCode CLI installation
run: opencode --version
Observed failure
With engine: { id: opencode, version: "1.18.4" }:
Error: opencode-ai's postinstall script was not run.
This occurs when using --ignore-scripts during installation, or when using a
package manager like pnpm that does not run postinstall scripts by default.
To fix this, run the postinstall script manually:
cd node_modules/opencode-ai && node postinstall.mjs
Or reinstall opencode-ai without the --ignore-scripts flag.
##[error]Process completed with exit code 1.
Reproduced on gh-aw v0.83.2 and v0.83.3, byte-identical in both. The two steps are unchanged on main as of v0.84.3.
Why the pinned version hides it
The postinstall script exists in both old and new opencode-ai; what changed is whether it is mandatory.
|
1.2.14 (gh-aw default) |
1.15.0 |
1.15.1 |
1.16.2 (latest) |
bin.opencode |
bin/opencode |
bin/opencode |
bin/opencode.exe |
bin/opencode.exe |
scripts.postinstall |
present |
present |
present |
present |
survives --ignore-scripts |
yes |
yes |
no |
no |
At tag v1.15.1 of anomalyco/opencode, packages/opencode/script/publish.ts began generating the published bin entry as a stub whose entire body is the error above followed by exit 1:
await Bun.file(`./dist/${pkg.name}/bin/${pkg.name}.exe`).write(
[
`echo "Error: ${pkg.name}-ai's postinstall script was not run." >&2`,
...
"exit 1",
At v1.15.0 the same script ships the real launcher instead (cp -r ./bin ./dist/${pkg.name}/bin), and that launcher resolves the native binary at runtime by walking node_modules, with the native binaries arriving as optionalDependencies -- which npm installs regardless of --ignore-scripts. So before 1.15.1 the postinstall was an optimization; from 1.15.1 it is the only thing that puts a working binary in place.
The v1.15.1 release notes do not describe this as a breaking change, which is likely why it has gone unnoticed here.
Environment
- gh-aw
v0.83.2, v0.83.3; steps confirmed unchanged at v0.84.3
engine: { id: opencode, version: "1.18.4" }
- runner
ubuntu-latest, workflow_dispatch
- AWF
v0.27.41 (repo main now pins v0.27.43), rootless
- Job order observed:
Install AWF binary -> Install OpenCode -> Verify OpenCode CLI installation (fails) -> all container steps skipped
Why a user cannot work around it
Root-level steps: are emitted before Install OpenCode in the agent job, so a user-supplied step cannot run the postinstall after the fact. There is no frontmatter knob to drop --ignore-scripts, and editing the compiled .lock.yml defeats the point of compiling. The only available workaround is to stay on a pre-1.15.1 opencode-ai.
Suggested fix
I want to be explicit that I am not asking for --ignore-scripts to be dropped globally. #30832 established that it is a deliberate supply-chain control -- the npm install runs on the host runner before the AWF sandbox starts, with the workflow's secrets in scope -- and PR #31379 restored it after a regression. That reasoning is sound and this report does not ask to weaken it.
Preferred: treat opencode the way crush was treated in #29735. That PR changed GetInstallationSteps to call GenerateNpmInstallSteps with runInstallScripts=true for the crush engine only, described as "mirroring how the Claude engine handles its install", and added a post-install crush --version step to force deferred binary downloads. The justification given there -- "requires post-install scripts to download native binaries. With --ignore-scripts, these scripts are skipped and the binary is never downloaded, causing the crush command to fail at runtime" -- now describes opencode-ai at 1.15.1+ exactly. opencode already has the equivalent verify step; that step is precisely what fails. The global default and every other engine stay untouched.
One nit worth not copying forward: the review on #29735 flagged the commandName / EngineConfig.Command block preceding versionStep as unreachable dead code, and it merged unfixed.
Alternative, if a narrower blast radius is preferred: keep --ignore-scripts and run only the top-level package's postinstall explicitly, so transitive dependency scripts still never execute:
- name: Install OpenCode
run: |
npm install --ignore-scripts -g opencode-ai@1.2.14
node "$(npm root -g)/opencode-ai/postinstall.mjs"
This is strictly tighter than the crush precedent, since it executes exactly one known script rather than the whole tree's. It does mean the compiler carries a package-specific path.
Either way, worth adding a smoke case pinned to a 1.15.1+ opencode-ai. The current smoke-opencode pin cannot catch this class of regression.
Context
Found while running a security probe that treats the agent as hostile and tests whether the provider credential is recoverable from inside the agent container. The probe is designed so that a run which does not happen cannot be reported as a pass -- which is how this surfaced as a hard stop rather than a silent clean sweep.
Summary
The compiled workflow for
engine: opencodeemits two adjacent steps that contradict each other. The install step skips npm lifecycle scripts; the verify step immediately requires the binary that only the skipped postinstall provides.This is masked today by the default version pin. gh-aw pins
opencode-ai@1.2.14, which predates the point at which that package's postinstall became load-bearing, sosmoke-opencodepasses. Any user who setsengine.versionto 1.15.1 or later -- including the currentlatest, 1.16.2 -- gets a guaranteed hard failure before the agent container starts.The generated steps
From
.github/workflows/smoke-opencode.lock.ymlonmain(lines 580-584, and again at 1608-1612 for the non-rootless job):Observed failure
With
engine: { id: opencode, version: "1.18.4" }:Reproduced on gh-aw
v0.83.2andv0.83.3, byte-identical in both. The two steps are unchanged onmainas ofv0.84.3.Why the pinned version hides it
The postinstall script exists in both old and new
opencode-ai; what changed is whether it is mandatory.latest)bin.opencodebin/opencodebin/opencodebin/opencode.exebin/opencode.exescripts.postinstall--ignore-scriptsAt tag
v1.15.1ofanomalyco/opencode,packages/opencode/script/publish.tsbegan generating the publishedbinentry as a stub whose entire body is the error above followed byexit 1:At
v1.15.0the same script ships the real launcher instead (cp -r ./bin ./dist/${pkg.name}/bin), and that launcher resolves the native binary at runtime by walkingnode_modules, with the native binaries arriving asoptionalDependencies-- which npm installs regardless of--ignore-scripts. So before 1.15.1 the postinstall was an optimization; from 1.15.1 it is the only thing that puts a working binary in place.The
v1.15.1release notes do not describe this as a breaking change, which is likely why it has gone unnoticed here.Environment
v0.83.2,v0.83.3; steps confirmed unchanged atv0.84.3engine: { id: opencode, version: "1.18.4" }ubuntu-latest,workflow_dispatchv0.27.41(repomainnow pinsv0.27.43), rootlessInstall AWF binary->Install OpenCode->Verify OpenCode CLI installation(fails) -> all container steps skippedWhy a user cannot work around it
Root-level
steps:are emitted beforeInstall OpenCodein the agent job, so a user-supplied step cannot run the postinstall after the fact. There is no frontmatter knob to drop--ignore-scripts, and editing the compiled.lock.ymldefeats the point of compiling. The only available workaround is to stay on a pre-1.15.1opencode-ai.Suggested fix
I want to be explicit that I am not asking for
--ignore-scriptsto be dropped globally. #30832 established that it is a deliberate supply-chain control -- the npm install runs on the host runner before the AWF sandbox starts, with the workflow's secrets in scope -- and PR #31379 restored it after a regression. That reasoning is sound and this report does not ask to weaken it.Preferred: treat opencode the way crush was treated in #29735. That PR changed
GetInstallationStepsto callGenerateNpmInstallStepswithrunInstallScripts=truefor the crush engine only, described as "mirroring how the Claude engine handles its install", and added a post-installcrush --versionstep to force deferred binary downloads. The justification given there -- "requires post-install scripts to download native binaries. With--ignore-scripts, these scripts are skipped and the binary is never downloaded, causing thecrushcommand to fail at runtime" -- now describesopencode-aiat 1.15.1+ exactly. opencode already has the equivalent verify step; that step is precisely what fails. The global default and every other engine stay untouched.One nit worth not copying forward: the review on #29735 flagged the
commandName/EngineConfig.Commandblock precedingversionStepas unreachable dead code, and it merged unfixed.Alternative, if a narrower blast radius is preferred: keep
--ignore-scriptsand run only the top-level package's postinstall explicitly, so transitive dependency scripts still never execute:This is strictly tighter than the crush precedent, since it executes exactly one known script rather than the whole tree's. It does mean the compiler carries a package-specific path.
Either way, worth adding a smoke case pinned to a 1.15.1+
opencode-ai. The currentsmoke-opencodepin cannot catch this class of regression.Context
Found while running a security probe that treats the agent as hostile and tests whether the provider credential is recoverable from inside the agent container. The probe is designed so that a run which does not happen cannot be reported as a pass -- which is how this surfaced as a hard stop rather than a silent clean sweep.