What happened?
When the DevTools feature is enabled (general.devtools: true in settings), the update check (and potentially other HTTP requests) fails because the ActivityLogger.patchGlobalFetch() interceptor strips headers from Request objects.
The patchGlobalFetch method in packages/cli/src/utils/activityLogger.ts replaces global.fetch with an interceptor. The interceptor reconstructs headers from init?.headers, but when the caller passes headers inside a Request object (the first argument) rather than in the init parameter, those headers are lost:
// activityLogger.ts, patchGlobalFetch()
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
// ...
const newInit = { ...init };
const headers = new Headers(init?.headers || {}); // ← Bug: ignores input.headers
headers.set(ACTIVITY_ID_HEADER, id);
newInit.headers = headers;
// ...
const response = await originalFetch(input, newInit); // ← headers from Request object are overridden
};
The ky HTTP library (used by package-json → latest-version → checkForUpdates) calls fetch(requestObject, {}), placing Authorization and Accept headers on the Request object. The interceptor creates new Headers({}), adds only the activity ID header, and passes that as newInit.headers — which overrides the Request object's headers when originalFetch is called.
This causes the update check to fail with a 403 when the npm registry requires authentication (e.g., private registries configured via .npmrc). It could also silently affect any other code path that uses globalThis.fetch with a Request object while devtools is enabled.
Reproduction steps
- Configure a private npm registry that requires authentication in
~/.npmrc
- Publish a package to that registry (or use any authenticated registry)
- Enable devtools: set
"general": { "devtools": true } in .gemini/settings.json
- Run
gemini interactively — the update check will fail with a 403
Alternatively, this can be observed by adding logging inside patchGlobalFetch:
// After: const headers = new Headers(init?.headers || {});
console.log('init?.headers:', init?.headers); // undefined or {}
console.log('input.headers:', input instanceof Request ? Object.fromEntries(input.headers) : 'N/A');
// Shows that input.headers has Authorization, but headers does not
Root cause
The interceptor assumes headers are always in the init parameter. Per the Fetch API spec, headers can be on the Request object (first argument) instead. When init.headers is not provided, the interceptor should fall back to the Request object's headers.
Suggested fix
// Before:
const headers = new Headers(init?.headers || {});
// After:
const inputHeaders = typeof input === 'object' && 'headers' in input
? input.headers
: undefined;
const headers = new Headers(init?.headers || inputHeaders || {});
Similarly, the method extraction should also consider the Request object:
// Before:
const method = (init?.method || 'GET').toUpperCase();
// After:
const inputMethod = typeof input === 'object' && 'method' in input
? input.method
: undefined;
const method = (init?.method || inputMethod || 'GET').toUpperCase();
What did you expect to happen?
The DevTools activity logger should transparently intercept fetch calls without modifying request semantics. Headers from Request objects should be preserved and forwarded to the original fetch implementation.
Client information
- Gemini CLI version: 0.33.2 (source tree; bug is in
packages/cli/src/utils/activityLogger.ts)
- Node.js: v24.13.0
- Platform: Linux
Login information
Google Account
Anything else we need to know?
- The bug only manifests when
general.devtools is true in settings (which enables the activity logger and its fetch interceptor).
- On the public npmjs.org registry, the stripped
Authorization header doesn't cause a visible failure because the registry is public. However, users with private registries (configured via .npmrc with scoped auth tokens) will see 403 errors on the update check.
- The
Accept header is also silently stripped, which could cause subtle content negotiation issues.
- The same bug likely affects the
body extraction — init?.body is checked but input.body (from the Request object) is not.
- Beyond the update check, any future code (or MCP server / extension) that calls
globalThis.fetch with a Request object while devtools is enabled would be affected.
What happened?
When the DevTools feature is enabled (
general.devtools: truein settings), the update check (and potentially other HTTP requests) fails because theActivityLogger.patchGlobalFetch()interceptor strips headers fromRequestobjects.The
patchGlobalFetchmethod inpackages/cli/src/utils/activityLogger.tsreplacesglobal.fetchwith an interceptor. The interceptor reconstructs headers frominit?.headers, but when the caller passes headers inside aRequestobject (the first argument) rather than in theinitparameter, those headers are lost:The
kyHTTP library (used bypackage-json→latest-version→checkForUpdates) callsfetch(requestObject, {}), placingAuthorizationandAcceptheaders on theRequestobject. The interceptor createsnew Headers({}), adds only the activity ID header, and passes that asnewInit.headers— which overrides theRequestobject's headers whenoriginalFetchis called.This causes the update check to fail with a 403 when the npm registry requires authentication (e.g., private registries configured via
.npmrc). It could also silently affect any other code path that usesglobalThis.fetchwith aRequestobject while devtools is enabled.Reproduction steps
~/.npmrc"general": { "devtools": true }in.gemini/settings.jsongeminiinteractively — the update check will fail with a 403Alternatively, this can be observed by adding logging inside
patchGlobalFetch:Root cause
The interceptor assumes headers are always in the
initparameter. Per the Fetch API spec, headers can be on theRequestobject (first argument) instead. Wheninit.headersis not provided, the interceptor should fall back to theRequestobject's headers.Suggested fix
Similarly, the
methodextraction should also consider theRequestobject:What did you expect to happen?
The DevTools activity logger should transparently intercept fetch calls without modifying request semantics. Headers from
Requestobjects should be preserved and forwarded to the originalfetchimplementation.Client information
packages/cli/src/utils/activityLogger.ts)Login information
Google Account
Anything else we need to know?
general.devtoolsistruein settings (which enables the activity logger and its fetch interceptor).Authorizationheader doesn't cause a visible failure because the registry is public. However, users with private registries (configured via.npmrcwith scoped auth tokens) will see 403 errors on the update check.Acceptheader is also silently stripped, which could cause subtle content negotiation issues.bodyextraction —init?.bodyis checked butinput.body(from theRequestobject) is not.globalThis.fetchwith aRequestobject while devtools is enabled would be affected.