Repositories / jai.git

jai.git

Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git

Branch

hide $XDG_RUNTIME_DIR by default

Author
David Mazieres <dm@uun.org>
Date
2026-03-26 20:32:56 -0700
Commit
959c17670628827b865522f505c911c57dabc13e
jai.1.md
index 2b3f853..b76a73d 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -152,6 +152,12 @@ environment before running the command.  For more complicated setup
 logic, you can use `setenv` to set the `BASH_ENV` environment variable
 to an initialization script to be sourced in non-interactive session.
 
+The `--dir`, `--xdir`, `--mask`, `--unmask`, `--setenv`, and
+`--storage` options will perform environment variable substitution for
+variable names contained within `${`...`}`, but the braces are
+required, unlike in the shell.  You can quote a literal `$` by
+preceding it with a backslash `\`.
+
 # EXAMPLES
 
 To install claude code in a jail called `claude`:
@@ -359,13 +365,6 @@ opencode`):
     If the argument contains `=`, then *var* is always treated as a
   variable, not a pattern, and it is assigned *value* in the jail.
 
-    If *value* contains the pattern `${`*envvar*`}`, it will be
-  replaced by the value of the environment variable *envvar* at the
-  time jai was invoked, or to *envval* of the previous `--setenv`
-  *envvar*`=`*envval* command if that `--setenv` has not been undone
-  by a subsequent `--unsetenv`.  If *value* contains `\`, it escapes
-  the next character.
-
 `--storage` *dir*
 : Specify an alternate location in which to store private home
   directories and overlays.  The default is `$JAI_CONFIG_DIR` if set,
@@ -373,9 +372,6 @@ opencode`):
   you may wish to use storage on a local file system, as NFS does not
   support the extended attributes required by overlay file systems.
 
-    Like `--setenv`, `--storage` expands `${`*envvar*`}` patterns and
-uses `\` to escape the next character.
-
 `--command` *bash-command*
 : jai launches the jailed program you specify by running "`/bin/bash
   -c` *bash-command* *cmd* *arg*...".  By default, *bash-command* just
jai.cc
index e57cfcd..5046367 100644
--- a/jai.cc
+++ b/jai.cc
@@ -336,6 +336,17 @@ Config::make_private_tmp()
 }
 
 Fd
+Config::make_private_run()
+{
+  Fd fd = ensure_dir(run_jai_user(), "tmp/.run" / sandbox_name_, 0700,
+                     kNoFollow, true);
+  if (xfstat(*fd).st_uid != user_cred_.uid_ &&
+      fchown(*fd, user_cred_.uid_, user_cred_.gid_))
+    syserr("{}: fchown", fdpath(*fd));
+  return fd;
+}
+
+Fd
 Config::make_private_passwd()
 {
   if (Fd fd = openat(run_jai_user(), "passwd", O_RDONLY | O_CLOEXEC))
@@ -430,6 +441,16 @@ Config::make_mnt_ns()
   Fd passwd;
   if (mode_ == kStrict)
     passwd = clone_tree(*make_private_passwd());
+  path xdgrun = std::format("/run/user/{}", user_cred_.uid_);
+  Fd rundir;
+  if (!grant_directories_.contains(xdgrun)) {
+    if (struct stat sb; !stat(xdgrun.c_str(), &sb)) {
+      check_user(sb, xdgrun);
+      rundir = clone_tree(*make_private_run());
+    }
+    else if (errno != ENOENT)
+      syserr("{}", xdgrun.string());
+  }
 
   Fd home;
   Fd mapns;
@@ -449,6 +470,8 @@ Config::make_mnt_ns()
   xmnt_setattr(*home, attr);
   if (passwd)
     xmnt_setattr(*passwd, attr);
+  if (rundir)
+    xmnt_setattr(*rundir, attr);
 
   if (unshare(CLONE_NEWNS))
     syserr("unshare(CLONE_NEWNS)");
@@ -468,6 +491,8 @@ Config::make_mnt_ns()
   xmnt_move(*home, -1, homepath_);
   if (passwd)
     xmnt_move(*passwd, -1, "/etc/passwd");
+  if (rundir)
+    xmnt_move(*rundir, -1, xdgrun);
 
   if (grant_cwd_) {
     if (!grant_directories_.contains(cwd())) {
@@ -504,7 +529,7 @@ Config::make_mnt_ns()
     restore_root = asuser();
     Fd dst = openat(-1, d.c_str(), O_DIRECTORY | O_PATH | O_CLOEXEC);
     if (!dst) {
-      if (mode_ != kStrict || (errno != EACCES && errno != ENOENT))
+      if (errno != EACCES && errno != ENOENT)
         syserr("{}", d.string());
       restore_root.reset();
       restore_root = asuser(sbcred);
@@ -980,14 +1005,16 @@ Config::opt_parser(bool dotjail)
       "casual|bare|strict");
   opts(
       "-d", "--dir",
-      [this](path d) {
+      [this](std::string_view arg) {
+        path d(expand(arg));
         grant_directories_.emplace(
             canonical(parsing_config_file_ ? homepath_ / d : d));
       },
       "Grant full access to DIR.", "DIR");
   opts(
       "-x", "--xdir",
-      [this](path d) {
+      [this](std::string_view arg) {
+        path d(expand(arg));
         grant_directories_.erase(
             canonical(parsing_config_file_ ? homepath_ / d : d));
       },
@@ -1014,14 +1041,18 @@ Config::opt_parser(bool dotjail)
   });
   opts(
       "--mask",
-      [this](path p) {
+      [this](std::string_view arg) {
+        path p(expand(arg));
         if (p.is_absolute())
           err<Options::Error>("{}: cannot mask an absolute path", p.string());
         mask_files_.emplace(std::move(p));
       },
       "Erase $HOME/FILE when first creating overlay home", "FILE");
   opts(
-      "--unmask", [this](path p) { mask_files_.erase(p); },
+      "--unmask", [this](std::string_view arg) {
+        path p(expand(arg));
+        mask_files_.erase(p);
+      },
       "Undo the effects of a previous --mask option", "FILE");
   opts(
       "--unsetenv",
jai.h
index bb3e3ec..83a63cd 100644
--- a/jai.h
+++ b/jai.h
@@ -166,6 +166,7 @@ struct Config {
   Fd make_blacklist(int dfd, path name);
   Fd make_home_overlay();
   Fd make_private_tmp();
+  Fd make_private_run();
   Fd make_private_passwd();
 
   const char *env_lookup(std::string_view var)