Repositories / jai.git

jai.git

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

Branch

expand environment variables from most recently set values

Author
David Mazieres <dm@uun.org>
Date
2026-03-24 23:15:07 -0700
Commit
ce2c0c8b30fc597847efc3dfecc5199cfe24abf2
jai.1.md
index 55315d7..e774964 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -101,13 +101,14 @@ Configuration comes from three sources: the command line, a `.conf`
 configuration file (which may include other files via `conf` options),
 and a `.jail` file specific to the named jail you are choosing.
 Command-line options override everything, and `.jail` files override
-`.conf` files.
+`.conf` files (except that `.jail` files cannot select a different
+jail).
 
 If you don't specify a `.conf` file on the command line with the `-C`
 option, and if *cmd* does not contain any slashes, jai will first try
 to use `$HOME/.jai/`*cmd*`.conf` if that file exists, and otherwise
-will use `$HOME/.jai/default.conf` (which it will create if needed).
-That way the `.conf` file can specify a jail name, and the
+will use `$HOME/.jai/default.conf` (which it will create if
+necessary).  That way the `.conf` file can specify a jail name and the
 `.jail` file can set the mode of the jail.
 
 The format of `.conf` and `.jail` configuration files is a series of
@@ -126,10 +127,12 @@ location.
 
 Within a configuration file, `conf` acts like an include directive,
 logically replacing the `conf` line with the contents of another
-configuration file.  Relative paths are relative to `$HOME/.jai/` (or
-`$JAI_CONFIG_DIR` if set).  jai creates a file `.defaults` with a
-sensible set of defaults you should probably include directly or
-indirectly in any `.conf` configuration file you write.
+configuration file.  Relative paths in `conf` include directives are
+relative to `$HOME/.jai/` (or `$JAI_CONFIG_DIR` if set).  jai creates
+a file `.defaults` with a sensible set of defaults you should probably
+include directly or indirectly as the first thing in any `.conf`
+configuration file you write.  (By including it first, anything else
+in your `.conf` file will override the defaults.)
 
 jai executes jailed programs with bash.  The `command` directive
 allows you to reconfigure the environment or add command-line options
@@ -137,14 +140,16 @@ to certain commands.  For instance, to use a python virtual
 environment in a jail, you might create a file `python.conf` with the
 following:
 
-    conf default.conf
+    conf .defaults
     mode strict
     dir venv
     jail python
     command source $HOME/venv/bin/activate; "$0" "$@"
 
 If you run `jai python`, this configuration file will load a virtual
-environment before running the command.
+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.
 
 # EXAMPLES
 
@@ -153,19 +158,44 @@ To install claude code in a jail called `claude`:
     curl -fsSL https://claude.ai/install.sh | \
         jai -D -mstrict -j claude bash
 
-To invoke claude code in that same jail, if `$HOME/.local/bin` is not
-already on your path:
+(Note the `bash` argument is optional, because jai runs `bash` by
+default.)  To invoke claude code in that same jail, if
+`$HOME/.local/bin` is not already on your path:
 
     PATH=$HOME/.local/bin:$PATH jai -j claude claude
 
 To make `jai claude` use the claude jail by default:
 
-    cat <<<EOF >$HOME/.jai/claude.conf
+    cat <<'EOF' >$HOME/.jai/claude.conf
     conf .defaults
     jail claude
-    command PATH=$HOME/.local/bin:$PATH "$0" "$@"
+    setenv PATH=${HOME}/.local/bin:${PATH}
+    EOF
+
+Now you can run `jai claude` to invoke claude code with access to the
+current working directory, and `jail -C claude` to get a shell with
+the same permissions as claude, so as to understand what claude is
+seeing.  To make `jai claudeyolo` run claude in dangerous mode, you
+could create another configuration file like this:
+
+    cat <<'EOF' > $HOME/.jai/claudeyolo.conf
+    # Start with claude's defaults
+    conf claude
+
+    # Add a shell function that will expand "claudeyolo" to the
+    # appropriate claude command for destroying your file system.
+    # (Note semicolon before the right brace and after, and backslash
+    # for continuation lines in the configuration file.)
+    command claudeyolo(){ \
+        claude --permission-mode bypassPermissions "$@"; \
+      }; "$0" "$@"
     EOF
 
+The author is not advocating doing the above!  But if you are going to
+use claude in dangerous mode, better to make the alias available only
+in jai, not in your outside shells, so you don't accidentally do it
+without jai.
+
 Suppose you want to make your X11 session available in the claude jail
 to facilitate pasting images into claude.  This significantly reduces
 security, so isn't necessarily a good idea, but you can do it by
@@ -175,7 +205,7 @@ directory and merging them into your claude jail.
     # Extract cookies outside jail, merge them inside jail
     xauth extract - $DISPLAY | jai -C claude xauth merge -
 
-    # Now copy a screen region to paste in claude
+    # Now copy a screen region to paste into claude...
     import png:- | xclip -selection clipboard -t image/png
 
 A safer way to do this is to write your screengrabs directly into the
@@ -330,8 +360,10 @@ opencode`):
 
     If *value* contains the pattern `${`*envvar*`}`, it will be
   replaced by the value of the environment variable *envvar* at the
-  time jai was invoked.  If value contains `\`, it escapes the next
-  character.
+  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
jai.cc
index 624cbe3..966d581 100644
--- a/jai.cc
+++ b/jai.cc
@@ -1036,7 +1036,7 @@ Config::opt_parser(bool dotjail)
       [this](std::string var) {
         if (auto pos = var.find('='); pos != var.npos) {
           auto var_eq_val = std::format("{}{}", var.substr(0, pos + 1),
-                                        var_expand(var.substr(pos + 1)));
+                                        expand(var.substr(pos + 1)));
           setenv_.insert_or_assign(var.substr(0, pos), var_eq_val);
         }
         else if (auto it = env_filter_.find(var); it != env_filter_.end())
@@ -1059,7 +1059,7 @@ Config::opt_parser(bool dotjail)
   opts(
       "--storage",
       [this](std::string_view s) {
-        auto sd = var_expand(s);
+        auto sd = expand(s);
         if (parsing_config_file_)
           storagedir_ = homepath_ / sd;
         else
jai.h
index b17812d..063800e 100644
--- a/jai.h
+++ b/jai.h
@@ -50,18 +50,6 @@ xfork(std::uint64_t flags = 0)
   syserr("clone3");
 }
 
-/*
-inline bool
-is_pid_stopped(pid_t pid)
-{
-  auto fullstat = read_file(-1, std::format("/proc/{}/stat", pid));
-  auto pos = fullstat.rfind(')');
-  if (pos == fullstat.npos)
-    err("/proc/PID/stat: wrong format");
-  return (std::string_view(fullstat).substr(pos, 3) == ") T");
-}
-*/
-
 inline sigset_t
 sigsingleton(int sig)
 {
@@ -177,6 +165,18 @@ struct Config {
   Fd make_private_tmp();
   Fd make_private_passwd();
 
+  const char *env_lookup(std::string_view var)
+  {
+    if (auto it = setenv_.find(var); it != setenv_.end())
+      if (auto pos = it->second.find('='); pos != it->second.npos)
+        return it->second.c_str() + pos + 1;
+    return env_or_empty(var);
+  }
+  std::string expand(std::string_view in)
+  {
+    return var_expand(in, [this](std::string_view v) { return env_lookup(v); });
+  }
+
   static bool name_ok(path p)
   {
     return p.is_relative() && std::ranges::distance(p.begin(), p.end()) == 1 &&