Welcome to the first chapter of the Migrations project tutorial!
In this chapter, we will explore how configuration data is organized within the application. Understanding where settings liveβand how to move themβis the foundation for writing safe and effective migrations.
Imagine you are defining rules for a society. You have three levels of government:
The Problem: In the early days of our application, we stored almost everything in "Federal Law" (Global Config). For example, if a user wanted to turn off auto-updates, that setting was saved in a machine-wide file.
The Use Case: What if a user wants strict security settings globally, but needs to relax permissions just for one specific project they are working on? If the setting is stuck in the Global scope, they can't do that.
The Solution: We use Migrations to move data from the rigid Global scope down to the more flexible User or Local scopes. This chapter explains how that hierarchy works and how to move data between layers.
The codebase distinguishes between three distinct scopes.
~/.claude.jsongetGlobalConfig()userSettings (managed internally)getSettingsForSource('userSettings')localSettings or projectConfiggetSettingsForSource('localSettings')
A common task in this project is taking a legacy setting from GlobalConfig and moving it to userSettings. Let's look at how we implement this "Federal to State" shift.
First, we read the global configuration to see if the old data exists.
import { getGlobalConfig } from '../utils/config.js'
export function migrateBypassPermissions() {
const globalConfig = getGlobalConfig()
// If the old setting isn't there, we don't need to do anything
if (!globalConfig.bypassPermissionsModeAccepted) {
return
}
// ... continue to Step 2
}
If we find the data, we write it to the new, more flexible location. We use a helper called updateSettingsForSource.
import { updateSettingsForSource } from '../utils/settings/settings.js'
// Move the preference to 'userSettings'
// Note: We often rename the key to be more descriptive during this move
updateSettingsForSource('userSettings', {
skipDangerousModePermissionPrompt: true,
})
Finally, once the data is safely in User Settings, we remove it from Global Config so it doesn't cause conflicts later.
import { saveGlobalConfig } from '../utils/config.js'
// Remove the old key from the global file
saveGlobalConfig(current => {
// Destructure to remove the specific key, keep the rest
const { bypassPermissionsModeAccepted, ...updatedConfig } = current
return updatedConfig
})
When a migration runs, it acts as a bridge between these scopes. Here is the flow of a standard "Global to User" migration.
Sometimes we need to move data from a Project Config (legacy) to Local Settings (modern). This allows for better handling of things like MCP (Model Context Protocol) servers.
Let's look at migrateEnableAllProjectMcpServersToSettings.ts.
We look at the current project configuration to see if it has old MCP approval fields.
import { getCurrentProjectConfig } from '../utils/config.js'
const projectConfig = getCurrentProjectConfig()
// Check for legacy fields
const hasEnableAll = projectConfig.enableAllProjectMcpServers !== undefined
// ... checks for other fields
Unlike global settings which might be simple booleans, local settings often involve lists (like arrays of enabled servers). We must merge carefully to avoid duplicates.
// Merge existing local settings with the old project config data
const updates = {
enabledMcpjsonServers: [
...new Set([
...existingEnabledServers, // Keep what we already have
...projectConfig.enabledMcpjsonServers, // Add legacy data
]),
],
}
updateSettingsForSource('localSettings', updates)
migrateAutoUpdatesToSettings.ts, we only migrate if the user explicitly disabled updates.GlobalConfig to remember that a migration has finished. We will cover this in depth in Idempotent Execution Guards.In this chapter, you learned that our configuration is hierarchical: Global, User, and Local/Project. Migrations are the tools we use to refine this hierarchy, moving rigid "Federal" rules down to flexible "State" or "Municipal" preferences.
In the next chapter, we will look at how we manage the evolution of the application's state over time.
Next Chapter: State Migration and Evolution
Generated by Code IQ