Skip to content

Instantly share code, notes, and snippets.

@Soupstraw
Last active May 15, 2026 13:04
Show Gist options
  • Select an option

  • Save Soupstraw/f0ce4dd2a0e86a3f294e0efa555f81db to your computer and use it in GitHub Desktop.

Select an option

Save Soupstraw/f0ce4dd2a0e86a3f294e0efa555f81db to your computer and use it in GitHub Desktop.
{ combinators, helpers, pkgs, ... }:
let
inherit (combinators) add-pkg-deps add-runtime compose noescape rw-bind set-env;
inherit (pkgs) lib;
bootstrap = with pkgs; [
nix
bashInteractive
coreutils
gitMinimal
cacert
];
# state.entry is a string per jail.nix, either lib.getExe (raw store path)
# or lib.escapeShellArg (single-quote-wrapped). Strip surrounding single
# quotes if present, then take the store-path prefix.
entryStorePath =
entry:
let
unquoted =
if lib.hasPrefix "'" entry && lib.hasSuffix "'" entry then
lib.removeSuffix "'" (lib.removePrefix "'" entry)
else
entry;
m = builtins.match "(/nix/store/[^/]+).*" unquoted;
in
if m == null then null else builtins.head m;
in
{
sig = "String -> [Package] -> Permission";
doc = ''
Persists `/nix` across all jails with the given name, so `nix develop`
and other store-mutating operations retain state between launches.
The persistent store lives on the host at
`~/.local/share/jail.nix/nix-root/<name>/nix`, and is bind-mounted to
`/nix` inside the jail. Reset by deleting that directory.
On first launch, the following are copied into the persistent store
via `nix copy` against a chroot-store URL:
* a small bootstrap closure (`nix`, `bash`, `coreutils`, `git`)
* the jail's entry binary (extracted from `state.entry`)
* everything in `extraPkgs`
`nix copy` walks transitive references, so each top-level path pulls
in its full closure. The bootstrap and `extraPkgs` are also added to
`$PATH`, and `$SHELL` is set to bash so interactive use works out of
the box.
Use with `reset` to drop `bind-nix-store-runtime-closure` from base
permissions — otherwise the host's store paths are read-only mounted
over the persistent store and confuse the jail's nix database.
Example:
```nix
jail "claude" pkgs.claude-code (c: with c; [
reset
base
fake-passwd
network
(persist-store "claude" [ ])
mount-cwd
])
```
'';
impl =
name: extraPkgs:
let
rootPath = helpers.dataDirSubPath "nix-root/${name}";
nixPath = helpers.dataDirSubPath "nix-root/${name}/nix";
explicitPkgs = bootstrap ++ extraPkgs;
explicitPathsList = map (p: p.outPath) explicitPkgs;
in
compose [
(state:
if lib.elem "bind-nix-store-runtime-closure" state.includedOnce then
throw ''
persist-store "${name}" conflicts with bind-nix-store-runtime-closure,
which read-only binds host /nix/store paths over the persistent store
and desyncs the jail's nix database.
Fix one of:
* Add `reset` (and re-add `base` / `fake-passwd` as needed) before
persist-store in this jail's combinator list.
* Override basePermissions when extending jail.nix to omit
bind-nix-store-runtime-closure:
jail-nix.lib.extend {
inherit pkgs;
basePermissions = combinators: with combinators; [ base fake-passwd ];
};
''
else state)
(rw-bind (noescape nixPath) "/nix")
(add-pkg-deps explicitPkgs)
(set-env "SHELL" "${pkgs.bashInteractive}/bin/bash")
(set-env "NIX_SSL_CERT_FILE" "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt")
(set-env "SSL_CERT_FILE" "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt")
(state:
let
entry = entryStorePath state.entry;
allPathsList = explicitPathsList ++ lib.optional (entry != null) entry;
allPathsStr = lib.escapeShellArgs allPathsList;
stampHash = builtins.substring 0 16 (builtins.hashString "sha256" allPathsStr);
in
add-runtime ''
ROOT=${rootPath}
STAMP="$ROOT/.jail-nix-bootstrapped-${stampHash}"
mkdir -p "$ROOT"
(
${pkgs.util-linux}/bin/flock 9
if [ ! -e "$STAMP" ]; then
${pkgs.nix}/bin/nix copy --no-check-sigs \
--to "local?root=$ROOT" \
${allPathsStr}
rm -f "$ROOT"/.jail-nix-bootstrapped-*
touch "$STAMP"
fi
) 9>"$ROOT/.jail-nix-bootstrap.lock"
'' state)
];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment