@@ -216,6 +216,31 @@ function getUserSettingsFilePath(): string {
216216 return path . join ( configDir , "settings.json" ) ;
217217}
218218
219+ /**
220+ * Mutex-serialised read-modify-write of a settings file with an atomic
221+ * temp-file + rename, refusing to overwrite unparseable content. Returning
222+ * `existing` unchanged from `mutate` skips the write.
223+ */
224+ async function updateSettingsFile (
225+ mutex : AsyncMutex ,
226+ filePath : string ,
227+ mutate : ( existing : ClaudeCodeSettings ) => ClaudeCodeSettings ,
228+ ) : Promise < ClaudeCodeSettings > {
229+ await mutex . acquire ( ) ;
230+ try {
231+ const existing = await readSettingsFileForUpdate ( filePath ) ;
232+ const next = mutate ( existing ) ;
233+ if ( next === existing ) {
234+ return existing ;
235+ }
236+ await fs . promises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
237+ await writeFileAtomic ( filePath , `${ JSON . stringify ( next , null , 2 ) } \n` ) ;
238+ return next ;
239+ } finally {
240+ mutex . release ( ) ;
241+ }
242+ }
243+
219244const userSettingsWriteMutex = new AsyncMutex ( ) ;
220245
221246export async function getUserSettingsEnvVar (
@@ -230,34 +255,31 @@ export async function getUserSettingsEnvVar(
230255 * (`<CLAUDE_CONFIG_DIR>/settings.json`, app-scoped since the host sets
231256 * CLAUDE_CONFIG_DIR). `buildSessionOptions` reads the merged env at every
232257 * session spawn, so changes apply to new and resumed sessions without any
233- * per-session threading. `value === undefined` deletes the key. Mirrors
234- * `addAllowRules`: mutex-serialised, atomic temp-file + rename.
258+ * per-session threading. `value === undefined` deletes the key.
235259 */
236260export async function setUserSettingsEnvVar (
237261 key : string ,
238262 value : string | undefined ,
239263) : Promise < void > {
240- await userSettingsWriteMutex . acquire ( ) ;
241- try {
242- const filePath = getUserSettingsFilePath ( ) ;
243- const existing = await readSettingsFileForUpdate ( filePath ) ;
244- const env = { ...existing . env } ;
245- if ( value === undefined ) {
246- delete env [ key ] ;
247- } else {
248- env [ key ] = value ;
249- }
250- const next : ClaudeCodeSettings = { ...existing } ;
251- if ( Object . keys ( env ) . length > 0 ) {
252- next . env = env ;
253- } else {
254- delete next . env ;
255- }
256- await fs . promises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
257- await writeFileAtomic ( filePath , `${ JSON . stringify ( next , null , 2 ) } \n` ) ;
258- } finally {
259- userSettingsWriteMutex . release ( ) ;
260- }
264+ await updateSettingsFile (
265+ userSettingsWriteMutex ,
266+ getUserSettingsFilePath ( ) ,
267+ ( existing ) => {
268+ const env = { ...existing . env } ;
269+ if ( value === undefined ) {
270+ delete env [ key ] ;
271+ } else {
272+ env [ key ] = value ;
273+ }
274+ const next : ClaudeCodeSettings = { ...existing } ;
275+ if ( Object . keys ( env ) . length > 0 ) {
276+ next . env = env ;
277+ } else {
278+ delete next . env ;
279+ }
280+ return next ;
281+ } ,
282+ ) ;
261283}
262284
263285export function getManagedSettingsPath ( ) : string {
@@ -468,27 +490,22 @@ export class SettingsManager {
468490 async addAllowRules ( rules : PermissionRuleValue [ ] ) : Promise < void > {
469491 if ( rules . length === 0 ) return ;
470492 if ( ! this . initialized ) await this . initialize ( ) ;
471- await this . writeMutex . acquire ( ) ;
472- try {
473- const filePath = this . getLocalSettingsPath ( ) ;
474- const existing = await readSettingsFileForUpdate ( filePath ) ;
475- const permissions : PermissionSettings = {
476- ...( existing . permissions ?? { } ) ,
477- } ;
478- const current = new Set ( permissions . allow ?? [ ] ) ;
479- for ( const rule of rules ) {
480- current . add ( formatRule ( rule ) ) ;
481- }
482- permissions . allow = Array . from ( current ) ;
483- const next : ClaudeCodeSettings = { ...existing , permissions } ;
484- await fs . promises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
485- await writeFileAtomic ( filePath , `${ JSON . stringify ( next , null , 2 ) } \n` ) ;
486-
487- this . localSettings = next ;
488- this . mergeAllSettings ( ) ;
489- } finally {
490- this . writeMutex . release ( ) ;
491- }
493+ this . localSettings = await updateSettingsFile (
494+ this . writeMutex ,
495+ this . getLocalSettingsPath ( ) ,
496+ ( existing ) => {
497+ const permissions : PermissionSettings = {
498+ ...( existing . permissions ?? { } ) ,
499+ } ;
500+ const current = new Set ( permissions . allow ?? [ ] ) ;
501+ for ( const rule of rules ) {
502+ current . add ( formatRule ( rule ) ) ;
503+ }
504+ permissions . allow = Array . from ( current ) ;
505+ return { ...existing , permissions } ;
506+ } ,
507+ ) ;
508+ this . mergeAllSettings ( ) ;
492509 }
493510
494511 hasPostHogExecApproval ( subTool : string ) : boolean {
@@ -505,27 +522,22 @@ export class SettingsManager {
505522 async addPostHogExecApproval ( subTool : string ) : Promise < void > {
506523 if ( ! subTool ) return ;
507524 if ( ! this . initialized ) await this . initialize ( ) ;
508- await this . writeMutex . acquire ( ) ;
509- try {
510- const filePath = this . getLocalSettingsPath ( ) ;
511- const existing = await readSettingsFileForUpdate ( filePath ) ;
512- const current = new Set ( existing . posthogApprovedExecTools ?? [ ] ) ;
513- if ( current . has ( subTool ) ) {
514- return ;
515- }
516- current . add ( subTool ) ;
517- const next : ClaudeCodeSettings = {
518- ...existing ,
519- posthogApprovedExecTools : Array . from ( current ) ,
520- } ;
521- await fs . promises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
522- await writeFileAtomic ( filePath , `${ JSON . stringify ( next , null , 2 ) } \n` ) ;
523-
524- this . localSettings = next ;
525- this . mergeAllSettings ( ) ;
526- } finally {
527- this . writeMutex . release ( ) ;
528- }
525+ this . localSettings = await updateSettingsFile (
526+ this . writeMutex ,
527+ this . getLocalSettingsPath ( ) ,
528+ ( existing ) => {
529+ const current = new Set ( existing . posthogApprovedExecTools ?? [ ] ) ;
530+ if ( current . has ( subTool ) ) {
531+ return existing ;
532+ }
533+ current . add ( subTool ) ;
534+ return {
535+ ...existing ,
536+ posthogApprovedExecTools : Array . from ( current ) ,
537+ } ;
538+ } ,
539+ ) ;
540+ this . mergeAllSettings ( ) ;
529541 }
530542
531543 async setCwd ( cwd : string ) : Promise < void > {
0 commit comments