Skip to content

Instantly share code, notes, and snippets.

@andir
Created January 20, 2026 13:36
Show Gist options
  • Select an option

  • Save andir/8c1488f422bc335850ecd3a2cb7ead14 to your computer and use it in GitHub Desktop.

Select an option

Save andir/8c1488f422bc335850ecd3a2cb7ead14 to your computer and use it in GitHub Desktop.
{ pkgs, lib, config, ... }:
let
inherit (lib) types;
sandboxOptions = types.submodule ({ name, ... }: {
options = {
packages = lib.mkOption {
type = types.listOf types.package;
default = [ ];
};
entryPoint = lib.mkOption {
type = types.nullOr types.str;
default = "bin/zsh";
};
includeBasePackages = lib.mkOption {
type = types.bool;
default = true;
};
persistent = lib.mkOption {
default = false;
type = types.bool;
};
# relative to unsandboxed HOME
stateDirectory = lib.mkOption {
default = ".sandbox/${name}";
type = types.str;
};
homeManagerModules = lib.mkOption {
default = [ ];
# type = types.listOf types.any;
};
nixpakModules = lib.mkOption {
default = [ ];
};
};
});
cfg = config.sandbox;
hasSandboxes = (builtins.length (builtins.attrNames cfg)) > 0;
basePackages = builtins.map (n: pkgs.${n}) [
"bashInteractive"
"zsh"
"coreutils-full"
"procps"
"gnugrep"
"ripgrep"
"strace"
];
mkSandbox = name: options:
let
homeManagerConfig =
if (options.homeManagerModules != [ ]) then
(pkgs.home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = options.homeManagerModules ++ [
{
# inherit configuration from outer home-manager
home.stateVersion = lib.mkDefault config.home.stateVersion;
home.username = lib.mkDefault config.home.username;
home.homeDirectory = lib.mkDefault config.home.homeDirectory;
}
];
}
).config
else null;
runtimePackage = pkgs.symlinkJoin {
name = "runtime-paths-${name}";
paths = options.packages
++ basePackages
++ (
if homeManagerConfig != null then
homeManagerConfig.home.packages
++ [ homeManagerConfig.home.activationPackage ]
else [ ]
);
};
in
pkgs.mkNixPak {
config = { sloth, lib, ... }: {
imports = options.nixpakModules ++ [
(lib.mkIf (options.persistent != false) {
bubblewrap.bind.rw = [
[
(sloth.concat' (sloth.concat' sloth.homeDir options.stateDirectory) "/home")
sloth.homeDir
]
];
})
(lib.mkIf (homeManagerConfig != null) {
bubblewrap.bind.ro =
let
mapDirectory = dir: if lib.hasPrefix homeManagerConfig.home.homeDirectory dir then dir else "${homeManagerConfig.home.homeDirectory}/${dir}";
in
lib.mapAttrsToList
(name: value: [
(builtins.toString value.source)
(mapDirectory name)
])
homeManagerConfig.home.file;
})
];
bubblewrap.bindEntireStore = false;
bubblewrap.env.PATH = "${runtimePackage}/bin";
bubblewrap.env.SHELL = "${runtimePackage}/bin/zsh";
bubblewrap.tmpfs = [ "/tmp" ];
bubblewrap.bind.ro = [
# [ "${runtimePackage}" "/run/current-system/sw" ]
[
"${runtimePackage}/bin/sh"
"/bin/sh"
]
[
(toString (pkgs.writeText "passwd" "${config.home.username}:x:1000:100::${config.home.homeDirectory}:${runtimePackage}/bin/zsh"))
"/etc/passwd"
]
];
app.package = runtimePackage;
app.binPath = options.entryPoint;
flatpak.appId = "de.rammhold.sandbox.${name}";
};
};
in
{
options.sandbox = lib.mkOption {
type = types.attrsOf sandboxOptions;
};
config = lib.mkIf hasSandboxes {
home.packages = lib.mapAttrsToList
(name: value:
pkgs.writeShellScriptBin "sandbox-${name}" ''
echo ${name}
exec ${(mkSandbox name value).config.script}/${value.entryPoint}
''
)
cfg;
home.file = lib.mapAttrs'
(name: c:
lib.nameValuePair "${c.stateDirectory}/home/.placeholder" { text = "# ignore me"; })
cfg;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment