Skip to content

Instantly share code, notes, and snippets.

@piousdeer
Last active August 2, 2025 23:37
Show Gist options
  • Save piousdeer/b29c272eaeba398b864da6abf6cb5daa to your computer and use it in GitHub Desktop.
Save piousdeer/b29c272eaeba398b864da6abf6cb5daa to your computer and use it in GitHub Desktop.
Create mutable files with home-manager and Nix
{
home.file."test-file" = {
text = "Hello world";
force = true;
mutable = true;
};
}
# 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);
};
}
{ 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;
});
}
@Yeshey
Copy link

Yeshey commented May 24, 2025

since updating to 25.05, a bunch of warnings are issued:

trace: Obsolete option programs.vscode.userTasks' is used. It was renamed to programs.vscode.profiles.default.userTasks'.
trace: Obsolete option programs.vscode.userSettings' is used. It was renamed to programs.vscode.profiles.default.userSettings'.
trace: Obsolete option programs.vscode.keybindings' is used. It was renamed to programs.vscode.profiles.default.keybindings'.
trace: Obsolete option programs.vscode.globalSnippets' is used. It was renamed to programs.vscode.profiles.default.globalSnippets'.
trace: Obsolete option programs.vscode.languageSnippets' is used. It was renamed to programs.vscode.profiles.default.languageSnippets'.

@Doosty
Copy link

Doosty commented Jul 22, 2025

Ive been using this module extension for awhile but it recently broke for me for some other reason, the home-manager-myuser activation service was failing on system startup because this module wasnt able to find symlinks to make into mutable files even though the symlinks exist afterwards when i look for them. I suspected some sort of race condition so i moved the execution into a regular systemd.user service. It works fine for now but it isnt as elegant as having the logic right inside home.activation after linkGeneration. Im not sure why links are not available to me even when specifying hm.dag.entryAfter [ "linkGeneration" ], maybe some interaction with impermanence bindfs. Note that this is a problem that started happening to me about ~1month ago, so perhaps some recent home-manager code change can also be at fault.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment