Skip to content

Instantly share code, notes, and snippets.

@piousdeer
Last active January 2, 2026 19:25
Show Gist options
  • Select an option

  • Save piousdeer/b29c272eaeba398b864da6abf6cb5daa to your computer and use it in GitHub Desktop.

Select an option

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;
});
}
@GatlenCulp
Copy link

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