Description
Any Next.js 16 app that imports @ai-sdk/harness-claude-code into server code fails next build. Turbopack is the default build bundler in Next 16, so this is the default path, not an opt-in one.
./node_modules/@ai-sdk/harness-claude-code/dist/index.js:74:5
Error: Module not found: Can't resolve '../bridge/' <dynamic>
72 | const candidates = [
73 | new URL(`./bridge/${name}`, import.meta.url),
> 74 | new URL(`../bridge/${name}`, import.meta.url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
75 | ];
readBridgeAsset in dist/index.js builds two candidate URLs. Resolved from dist/index.js, the second one points at <pkg>/bridge/. The published tarball only ships dist/bridge/:
$ ls node_modules/@ai-sdk/harness-claude-code
CHANGELOG.md LICENSE README.md dist package.json src
$ ls node_modules/@ai-sdk/harness-claude-code/dist
bridge index.d.ts index.js index.js.map
So the second candidate can never resolve in an installed package. At runtime that is harmless, since the first candidate returns and the second only runs on a caught ENOENT. But Turbopack statically analyzes both dynamic paths and treats the unresolvable one as a hard build error.
It is bundler-specific, which is probably why it has gone unreported. Same app, same package, only the bundler changed:
| bundler |
result |
| Turbopack (Next 16 default) |
Module not found: Can't resolve '../bridge/' <dynamic> |
webpack (next build --webpack) |
✓ Compiled successfully in 2.2s, no warning |
File tracing is not the problem. @vercel/nft 1.11.0 traces all four bridge assets (package.json, pnpm-lock.yaml, pnpm-workspace.yaml, index.mjs) from a plain import, so the template literal itself is fine. It is only the candidate that points outside the shipped layout.
Not a regression. I pulled tarballs for 1.0.1, 1.0.10, 1.0.30, 1.0.50, 1.0.65, 1.0.70, 1.0.73, 1.0.76, 1.0.79, 1.0.82, 1.0.85, 1.0.89 and 1.0.97, and ../bridge/ is in every one. This looks like an omission from the start rather than something that broke recently.
Workaround
Adding the package to serverExternalPackages in next.config.ts avoids it, which is what we have been doing. That works, but it forces the whole dependency chain external for a reason that has nothing to do with wanting it external.
Possible fixes
- Drop the
../bridge/ candidate. It cannot resolve in any published install, so it is dead in the only layout users get.
- If it exists for a source or monorepo layout, gate it so only the shipped path survives into
dist, or derive both candidates from one base that always resolves.
- Separately, consider exposing the assets through an
exports subpath, for example "./bridge/*": "./dist/bridge/*". Today exports is only "." and "./package.json". Consumers who need the bridge lockfile have to require.resolve the package.json and then path.join into dist/bridge, which reaches into internals that are not part of the public surface. We do this to hash the bridge lockfile so a sandbox snapshot invalidates when the pinned Claude Code version changes.
What I did not check
Whether other bundlers (Vite, Rollup, esbuild) reject it the same way. I only tested Turbopack and webpack through Next. Also only tested on Node 23.5.0. It is a build-time static analysis error, so the Node version should not matter, but I did not verify that on another version.
Reproduction
Four files, no next.config:
mkdir repro && cd repro
npm init -y
npm install next@latest react@latest react-dom@latest @ai-sdk/harness-claude-code@latest
mkdir -p app/api/probe
app/api/probe/route.js:
import { createClaudeCode } from '@ai-sdk/harness-claude-code';
export const dynamic = 'force-dynamic';
export async function GET() {
return Response.json({ ok: typeof createClaudeCode });
}
app/layout.js:
export default function RootLayout({ children }) {
return <html lang="en"><body>{children}</body></html>;
}
app/page.js:
export default function Page() {
return <main>repro</main>;
}
Then:
npx next build # fails, Module not found: '../bridge/'
npx next build --webpack # passes
AI SDK Version
| package |
version |
@ai-sdk/harness-claude-code |
1.0.97 |
@ai-sdk/harness |
1.0.93 (transitive) |
next |
16.3.3 |
react / react-dom |
19.2.8 |
node |
23.5.0 |
npm |
10.9.2 |
| OS |
macOS 26.6.1 |
Also reproduces on next@16.3.2 with @ai-sdk/harness-claude-code@1.0.89 under pnpm, so it is not specific to the flat npm layout or to the newest Next patch.
Code of Conduct
Description
Any Next.js 16 app that imports
@ai-sdk/harness-claude-codeinto server code failsnext build. Turbopack is the default build bundler in Next 16, so this is the default path, not an opt-in one.readBridgeAssetindist/index.jsbuilds two candidate URLs. Resolved fromdist/index.js, the second one points at<pkg>/bridge/. The published tarball only shipsdist/bridge/:So the second candidate can never resolve in an installed package. At runtime that is harmless, since the first candidate returns and the second only runs on a caught ENOENT. But Turbopack statically analyzes both dynamic paths and treats the unresolvable one as a hard build error.
It is bundler-specific, which is probably why it has gone unreported. Same app, same package, only the bundler changed:
Module not found: Can't resolve '../bridge/' <dynamic>next build --webpack)✓ Compiled successfully in 2.2s, no warningFile tracing is not the problem.
@vercel/nft1.11.0 traces all four bridge assets (package.json,pnpm-lock.yaml,pnpm-workspace.yaml,index.mjs) from a plain import, so the template literal itself is fine. It is only the candidate that points outside the shipped layout.Not a regression. I pulled tarballs for 1.0.1, 1.0.10, 1.0.30, 1.0.50, 1.0.65, 1.0.70, 1.0.73, 1.0.76, 1.0.79, 1.0.82, 1.0.85, 1.0.89 and 1.0.97, and
../bridge/is in every one. This looks like an omission from the start rather than something that broke recently.Workaround
Adding the package to
serverExternalPackagesinnext.config.tsavoids it, which is what we have been doing. That works, but it forces the whole dependency chain external for a reason that has nothing to do with wanting it external.Possible fixes
../bridge/candidate. It cannot resolve in any published install, so it is dead in the only layout users get.dist, or derive both candidates from one base that always resolves.exportssubpath, for example"./bridge/*": "./dist/bridge/*". Todayexportsis only"."and"./package.json". Consumers who need the bridge lockfile have torequire.resolvethe package.json and thenpath.joinintodist/bridge, which reaches into internals that are not part of the public surface. We do this to hash the bridge lockfile so a sandbox snapshot invalidates when the pinned Claude Code version changes.What I did not check
Whether other bundlers (Vite, Rollup, esbuild) reject it the same way. I only tested Turbopack and webpack through Next. Also only tested on Node 23.5.0. It is a build-time static analysis error, so the Node version should not matter, but I did not verify that on another version.
Reproduction
Four files, no
next.config:app/api/probe/route.js:app/layout.js:app/page.js:Then:
AI SDK Version
@ai-sdk/harness-claude-code@ai-sdk/harnessnextreact/react-domnodenpmAlso reproduces on
next@16.3.2with@ai-sdk/harness-claude-code@1.0.89under pnpm, so it is not specific to the flat npm layout or to the newest Next patch.Code of Conduct