{"id":"7e3a5d6eec1a6386","repo":"evanw/esbuild","slug":"mutating-absworkingdir-is-not-allowed","errorCode":null,"errorMessage":"Mutating \"AbsWorkingDir\" is not allowed","messagePattern":"Mutating \"AbsWorkingDir\" is not allowed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"pkg/api/api_impl.go","lineNumber":916,"sourceCode":"\t\t// for performance).\n\t\tDoNotCache: true,\n\t})\n\tif err != nil {\n\t\tlog := logger.NewStderrLog(logOptions)\n\t\tlog.AddError(nil, logger.Range{}, err.Error())\n\t\treturn nil, convertMessagesToPublic(logger.Error, log.Done(), logOptions.PathStyle)\n\t}\n\n\t// Do not re-evaluate plugins when rebuilding. Also make sure the working\n\t// directory doesn't change, since breaking that invariant would break the\n\t// validation that we just did above.\n\tcaches := cache.MakeCacheSet()\n\tlog := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, logOptions.Overrides)\n\tonEndCallbacks, onDisposeCallbacks, finalizeBuildOptions := loadPlugins(&buildOpts, realFS, log, caches)\n\toptions, entryPoints := validateBuildOptions(buildOpts, log, realFS)\n\tfinalizeBuildOptions(&options)\n\tif buildOpts.AbsWorkingDir != absWorkingDir {\n\t\tpanic(\"Mutating \\\"AbsWorkingDir\\\" is not allowed\")\n\t}\n\n\t// If we have errors already, then refuse to build any further. This only\n\t// happens when the build options themselves contain validation errors.\n\tmsgs := log.Done()\n\tif log.HasErrors() {\n\t\tif logOptions.LogLevel < logger.LevelSilent {\n\t\t\t// Print all deferred validation log messages to stderr. We defer all log\n\t\t\t// messages that are generated above because warnings are re-printed for\n\t\t\t// every rebuild and we don't want to double-print these warnings for the\n\t\t\t// first build.\n\t\t\tstderr := logger.NewStderrLog(logOptions)\n\t\t\tfor _, msg := range msgs {\n\t\t\t\tstderr.AddMsg(msg)\n\t\t\t}\n\t\t\tstderr.Done()\n\t\t}\n\t\treturn nil, convertMessagesToPublic(logger.Error, msgs, options.LogPathStyle)","sourceCodeStart":898,"sourceCodeEnd":934,"githubUrl":"https://github.com/evanw/esbuild/blob/6ff1d8b0d8c134e867a397eef39702a223ebef9e/pkg/api/api_impl.go#L898-L934","documentation":"A panic raised at the end of esbuild's build-context setup when, after running plugin setup (loadPlugins) and option validation (validateBuildOptions), the value of buildOpts.AbsWorkingDir differs from the snapshot taken before those calls. esbuild validates AbsWorkingDir against RealFS first, then forbids any plugin or callback from mutating it, because the file-system object is already built around that directory and a silent change would invalidate path resolution. So this panic indicates a plugin (or code calling esbuild's internal API) is rewriting options.AbsWorkingDir during setup.","triggerScenarios":"A plugin's setup function (or a Go-side hook between loadPlugins and the post-check) mutates buildOpts.AbsWorkingDir — e.g. assigns to options.AbsWorkingDir, or shares a pointer to the options struct and edits it. Reproducible in the Go API where the BuildOptions struct is passed by value but its fields (slices/maps/pointers) can still alias the caller's memory. Not reachable from the JS API, which serialises options over the service protocol.","commonSituations":"Writing an esbuild Go plugin that 'helpfully' normalises the working directory in its setup; wrapping Build() and modifying the options struct after passing it in (race); caching a *BuildOptions pointer and editing AbsWorkingDir on rebuild; migrating from a version where this mutation was tolerated.","solutions":["Do not mutate AbsWorkingDir inside plugin setup callbacks — resolve it once before calling Build/Context and never touch it again.","If a plugin needs the working directory, read it from the args passed to onResolve/onLoad rather than mutating options.","Pass a fresh copy of BuildOptions (deep-copied if it shares slices/maps) to each Build call to prevent aliasing.","Audit any wrapper around esbuild.Build that holds a pointer to options and confirm it does not write to AbsWorkingDir."],"exampleFix":"// before (Go)\nplugin := api.Plugin{\n  Name: \"bad\",\n  Setup: func(b api.PluginBuild) {\n    // mutating shared options during setup -> panic\n    *(*string)(unsafe.Pointer(&sharedOpts.AbsWorkingDir)) = \"/new\"\n  },\n}\n\n// after\n// Resolve absWorkingDir ONCE before build and never mutate it after.\nabs, _ := filepath.Abs(\"./src\")\nopts.AbsWorkingDir = abs\nctx, _ := api.Build(opts)  // plugins may read, never write, AbsWorkingDir","handlingStrategy":"validation","validationCode":"// Treat AbsWorkingDir as immutable after the Build call begins.\n// Resolve it once, freeze the options object, and pass the frozen copy.\nimport * as path from 'path'\nfunction prepareOpts(raw) {\n  const opts = { ...raw, absWorkingDir: path.resolve(raw.absWorkingDir || process.cwd()) }\n  return Object.freeze(opts) // shallow freeze discourages mutation of top-level fields\n}\nconst opts = prepareOpts(raw)","typeGuard":"// Go: ensure no plugin hook is given a mutable pointer to BuildOptions.AbsWorkingDir.\n// (Field is a value type string; the risk is sharing the parent struct pointer.)\nfunc isImmutableAfterBuild(opts *api.BuildOptions, snapshot string) bool {\n  return opts.AbsWorkingDir == snapshot\n}","tryCatchPattern":"try {\n  const ctx = await esbuild.context(opts)\n} catch (e) {\n  if (/Mutating .AbsWorkingDir. is not allowed/i.test(String(e?.message || e))) {\n    console.error('A plugin or wrapper mutated options.absWorkingDir during setup.')\n  }\n  throw e\n}","preventionTips":["Resolve absWorkingDir once before Build and never write it again.","Plugins should read the working directory from onResolve/onLoad args, not mutate options.","Pass a fresh, deep-copied BuildOptions to each Build/Context call to prevent aliasing.","Avoid sharing a *BuildOptions pointer with code that may edit it on rebuild."],"tags":["api","working-dir","plugin","panic","invariant","mutation","go-api"],"analyzedSha":"6ff1d8b0d8c134e867a397eef39702a223ebef9e","analyzedAt":"2026-08-03T19:42:38.433Z","schemaVersion":2}