Last active
January 2, 2026 19:25
-
-
Save piousdeer/b29c272eaeba398b864da6abf6cb5daa to your computer and use it in GitHub Desktop.
Create mutable files with home-manager and Nix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| home.file."test-file" = { | |
| text = "Hello world"; | |
| force = true; | |
| mutable = true; | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This module extends home.file, xdg.configFile and xdg.dataFile with the `mutable` option. | |
| { config, lib, ... }: | |
| let | |
| fileOptionAttrPaths = | |
| [ [ "home" "file" ] [ "xdg" "configFile" ] [ "xdg" "dataFile" ] ]; | |
| in { | |
| options = let | |
| mergeAttrsList = builtins.foldl' (lib.mergeAttrs) { }; | |
| fileAttrsType = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { | |
| options.mutable = lib.mkOption { | |
| type = lib.types.bool; | |
| default = false; | |
| description = '' | |
| Whether to copy the file without the read-only attribute instead of | |
| symlinking. If you set this to `true`, you must also set `force` to | |
| `true`. Mutable files are not removed when you remove them from your | |
| configuration. | |
| This option is useful for programs that don't have a very good | |
| support for read-only configurations. | |
| ''; | |
| }; | |
| })); | |
| in mergeAttrsList (map (attrPath: | |
| lib.setAttrByPath attrPath (lib.mkOption { type = fileAttrsType; })) | |
| fileOptionAttrPaths); | |
| config = { | |
| home.activation.mutableFileGeneration = let | |
| allFiles = (builtins.concatLists (map | |
| (attrPath: builtins.attrValues (lib.getAttrFromPath attrPath config)) | |
| fileOptionAttrPaths)); | |
| filterMutableFiles = builtins.filter (file: | |
| (file.mutable or false) && lib.assertMsg file.force | |
| "if you specify `mutable` to `true` on a file, you must also set `force` to `true`"); | |
| mutableFiles = filterMutableFiles allFiles; | |
| toCommand = (file: | |
| let | |
| source = lib.escapeShellArg file.source; | |
| target = lib.escapeShellArg file.target; | |
| in '' | |
| $VERBOSE_ECHO "${source} -> ${target}" | |
| $DRY_RUN_CMD cp --remove-destination --no-preserve=mode ${source} ${target} | |
| ''); | |
| command = '' | |
| echo "Copying mutable home files for $HOME" | |
| '' + lib.concatLines (map toCommand mutableFiles); | |
| in (lib.hm.dag.entryAfter [ "linkGeneration" ] command); | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { config, pkgs, lib, ... }: | |
| let | |
| # Path logic from: | |
| # https://github.com/nix-community/home-manager/blob/3876cc613ac3983078964ffb5a0c01d00028139e/modules/programs/vscode.nix | |
| cfg = config.programs.vscode; | |
| vscodePname = cfg.package.pname; | |
| configDir = { | |
| "vscode" = "Code"; | |
| "vscode-insiders" = "Code - Insiders"; | |
| "vscodium" = "VSCodium"; | |
| }.${vscodePname}; | |
| userDir = if pkgs.stdenv.hostPlatform.isDarwin then | |
| "Library/Application Support/${configDir}/User" | |
| else | |
| "${config.xdg.configHome}/${configDir}/User"; | |
| configFilePath = "${userDir}/settings.json"; | |
| tasksFilePath = "${userDir}/tasks.json"; | |
| keybindingsFilePath = "${userDir}/keybindings.json"; | |
| snippetDir = "${userDir}/snippets"; | |
| pathsToMakeWritable = lib.flatten [ | |
| (lib.optional (cfg.userTasks != { }) tasksFilePath) | |
| (lib.optional (cfg.userSettings != { }) configFilePath) | |
| (lib.optional (cfg.keybindings != [ ]) keybindingsFilePath) | |
| (lib.optional (cfg.globalSnippets != { }) | |
| "${snippetDir}/global.code-snippets") | |
| (lib.mapAttrsToList (language: _: "${snippetDir}/${language}.json") | |
| cfg.languageSnippets) | |
| ]; | |
| in { | |
| home.file = lib.genAttrs pathsToMakeWritable (_: { | |
| force = true; | |
| mutable = true; | |
| }); | |
| } |
I was having some issues with the VSCode module conflicting with home.file, I think because I configure VSCode through home-manager program options rather than home.file. Not very familiar with nix but I / Claude generated this alternative which works (at least on nix-darwin):
# vscode-mutability.nix
# This module makes VS Code config files mutable (editable after generation)
# by replacing the nix-store symlinks with writable copies after home-manager
# creates them.
#
# Unlike using the `mutable` home.file option, this approach doesn't conflict
# with the VS Code module's file definitions since we just run an activation
# script after linkGeneration.
{ config, pkgs, lib, ... }:
let
cfg = config.programs.vscode.profiles.default;
configDir = "Code";
userDir = if pkgs.stdenv.hostPlatform.isDarwin then
"Library/Application Support/${configDir}/User"
else
"${config.xdg.configHome}/${configDir}/User";
# List of files to make mutable
filesToMakeMutable = lib.flatten [
(lib.optional (cfg.userSettings != { }) "${userDir}/settings.json")
(lib.optional (cfg.keybindings != [ ]) "${userDir}/keybindings.json")
(lib.optional (cfg.userTasks != { }) "${userDir}/tasks.json")
];
# Generate the shell commands to replace symlinks with copies
makeFileMutable = file: ''
target="$HOME/${file}"
if [ -L "$target" ]; then
real_source=$(readlink "$target")
echo "Making mutable: $target"
rm "$target"
cp "$real_source" "$target"
chmod u+w "$target"
elif [ -f "$target" ]; then
echo "Already mutable: $target"
fi
'';
in
{
home.activation.vscodeFileMutability = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
echo "Making VS Code config files mutable..."
${lib.concatMapStrings makeFileMutable filesToMakeMutable}
'';
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MWAH I love you, this worked for me.