Commit Diff


commit - c2ef7846314b3e8c70adc3a313cef7da32995f57
commit + 6dfb427fbfa52cbb037b31445b7e596c3f877075
blob - f7c8555345e82288b69856537a76e184616c5216
blob + f26d533171dc19fa023971ebbf7818c8298d912a
--- README.md
+++ README.md
@@ -1,71 +1,68 @@
 # pisol
 
-Run commands in a Landrun sandbox and manage each project's isolation settings.
-The terminal interface is written in Chez Scheme.
+Run commands in a Landrun sandbox. Run `pisol` with no command to manage the
+current project's isolation settings.
 
-`pisol` uses a trusted local `.isolate` Bash file. It denies outbound TCP by
-default. Use the editor to add writable and executable directories, and allow
-connections to specific TCP ports.
+`pisol` reads trusted Bash from `.isolate` in the current directory. It denies
+outbound TCP by default.
 
-## Run
+## Requirements
 
-You need Bash, [Landrun](https://github.com/Zouuup/landrun), Chez Scheme 10.4
-or later, and Make.
+You need:
 
-Install `fzf` to search for directories. Without it, enter a path directly.
+- Bash
+- [Landrun](https://github.com/Zouuup/landrun)
+- Chez Scheme 10.4 or later
+- Make
 
+## Start
+
+Build and run the editor:
+
 ```sh
 make tui
-make test
 ```
 
-After installation, open the editor in the working directory:
+Run a command in the current project's sandbox:
 
 ```sh
-pisol
-```
-
-Run a command in that directory sandbox:
-
-```sh
 pisol your-command --with arguments
 ```
 
-Use `pisol -- COMMAND` when a command name conflicts with a pisol option.
+Use `pisol -- COMMAND` when the command name is also a pisol option.
 
-The command receives your environment, `PROMPT_ENV_INDICATOR=isolated` and
-`ISOLATE_ENV`, set to the resolved working directory. If you run `pisol COMMAND`
-inside an existing pisol sandbox, it warns you and runs the command directly.
+The command receives your environment and these variables:
 
-## Terminal interface commands
+- `PROMPT_ENV_INDICATOR=isolated`
+- `ISOLATE_ENV`, set to the resolved project directory
 
+If `ISOLATE_ENV` is already set, pisol warns you and runs the command directly.
+It does not create a nested Landlock domain.
+
+## Use the editor
+
+The editor accepts these commands:
+
 ```text
-a          fuzzy-find or enter a writable directory path
-r          choose and remove a writable directory
-p PORT     allow outbound TCP connections to a destination port
-d          enter and remove an outbound TCP port
-c          show global and local configuration file contents
-s          save
-?          help
-x          exit (discards any unsaved changes)
+a          add a writable directory
+r          remove a writable directory
+p PORT     allow outbound TCP to PORT
+d          remove an outbound TCP port
+c          show configuration files
+?          show help
+x          save and exit
 ```
 
-After you select `a`, enter a base path such as `/home/miro`. Press Enter to
-start in your home directory. `fzf` shows the path and its immediate child
-directories. Press Enter to open a directory. Press Alt+Enter to select the
-directory under the cursor. Press Ctrl-S to save the current directory. Press
-Escape to use the path you entered.
+When you add a directory, the input starts at your home directory. Press Tab to
+complete a path. Press Tab twice to list matches. Press Enter to add the path.
 
-You cannot add the baseline writable tree again.
+You cannot add the project's writable tree again.
 
-Set `NO_COLOR=1` to disable colour. `COLUMNS` and `LINES` are respected by the
-terminal environment. Set `ISOLATE_EXTRA_CONFIG` to use a config path other
-than `CURRENT_DIRECTORY/.isolate`.
+## Configuration
 
-## Configuration and trust
+`.isolate` is trusted Bash code. Only use configuration files you trust.
 
-`.isolate` is trusted Bash code. The launcher sources it. A generated file
-looks like this:
+A generated file looks like this:
 
 ```bash
 args+=(
@@ -75,30 +72,24 @@ args+=(
 ```
 
 A global init file at `~/.config/pisol/init` runs before the local `.isolate`
-file. Put shared grants there. Project settings build on those grants. Set
-`PISOL_INIT_CONFIG` to use a different global init file.
+file. Use it for shared grants. Set `PISOL_INIT_CONFIG` to use another path.
 
-The source tree includes a permissive example init at `config/pisol/init`.
-Copy it to `~/.config/pisol/init` and change it for your needs. Remove
+The source tree includes an example init file at `config/pisol/init`. Copy it
+to `~/.config/pisol/init` and change it for your needs. Remove
 `--unrestricted-network` to deny outbound TCP by default.
 
-The editor evaluates existing custom `.isolate` code. It manages the resulting
-`--rwx` and `--connect-tcp` values and keeps the other Landrun arguments.
+pisol evaluates custom `.isolate` files and manages their `--rwx` and
+`--connect-tcp` arguments. Saving replaces custom code with a static argument
+list. This removes comments and dynamic logic.
 
-Saving custom code needs confirmation. It replaces the code with a static
-argument array, so it removes comments and dynamic logic. The editor shows
-global grants but does not let you change them. Add or remove project grants
-instead.
+The project tree is readable, writable and executable. Temporary storage is
+writable. System configuration, shared data and standard runtime paths are
+read-only. pisol grants the standard null, zero, random and terminal devices.
+It adds a Wayland socket when `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR` are set.
 
-The default policy gives the working tree read, write and execute access. It
-also gives write access to temporary storage. System configuration and shared
-data are read-only. Standard runtime and absolute `PATH` directories are
-readable and executable.
+Set `NO_COLOR=1` to disable colour. Set `ISOLATE_EXTRA_CONFIG` to use a local
+configuration file other than `CURRENT_DIRECTORY/.isolate`.
 
-The policy allows the standard null, zero, random and terminal devices. It does
-not allow unrestricted networking. It adds a Wayland socket when
-`WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR` are set.
-
 ## Build and install
 
 ```sh
@@ -108,7 +99,8 @@ make install
 make copy-config
 ```
 
-`make install` defaults to `/usr/local`; use `sudo make install` only when your
-chosen destination requires it.
-Set `PREFIX`, `DESTDIR`, `BINDIR`, `LIBEXECDIR`, `MANDIR`, or `SCHEME` to
-override the defaults. See `pisol(1)` after installation.
+`make install` uses `/usr/local` by default. Use `sudo make install` only when
+your chosen destination needs it.
+
+Set `PREFIX`, `DESTDIR`, `BINDIR`, `LIBEXECDIR`, `MANDIR` or `SCHEME` to change
+the build or installation paths.
blob - 11d180712ace8005b1e22e65eed4c54e881ab6e2
blob + ff730a838058e6ffc6f53adc6d606321ff25dad7
--- doc/pisol.1
+++ doc/pisol.1
@@ -1,6 +1,6 @@
-.TH PISOL 1 "2026-08-27" "pisol" "User Commands"
+.TH PISOL 1 "2026-08-28" "pisol" "User Commands"
 .SH NAME
-pisol \- configure and run working-directory Landrun isolation
+pisol \- run commands in a Landrun sandbox
 .SH SYNOPSIS
 .B pisol
 .br
@@ -13,72 +13,68 @@ pisol \- configure and run working-directory Landrun i
 .RI [ ARG ...]
 .SH DESCRIPTION
 .B pisol
-opens a Chez Scheme terminal editor for the current directory's isolation
-settings.  With a command, it replaces itself with
-.BR landrun (1)
-and runs the command in a sandbox for the current directory.  Use
+opens a terminal editor for the current project's isolation settings. With a
+command, it runs that command in a Landrun sandbox for the current directory.
+Use
 .B --
 before a command whose name conflicts with a pisol option.
 .PP
-The resolved current directory defines the writable tree.  The configuration
-path is
+The current directory is the writable project tree. pisol reads its
+configuration from
 .I CURRENT_DIRECTORY/.isolate
 unless
 .B ISOLATE_EXTRA_CONFIG
 is set.
 .SH ISOLATION POLICY
-The working tree is readable, writable and executable.  Temporary storage is
-writable.  Standard runtime and absolute PATH directories are readable and
-executable.  System configuration and shared data are read-only.  The standard
+The project tree is readable, writable and executable. Temporary storage is
+writable. Standard runtime paths and absolute PATH directories are readable and
+executable. System configuration and shared data are read-only. The standard
 null, zero, random and controlling-terminal devices are writable.
 .PP
-Outbound TCP is denied by default.  Only destination ports configured with
-.B --connect-tcp
-are granted.  Scoped IPC restrictions are relaxed, and the current Wayland
-socket is granted when its environment variables are available.
+Outbound TCP is denied by default. pisol grants only destination ports listed
+with
+.B --connect-tcp .
+It relaxes scoped IPC restrictions. It grants the current Wayland socket when
+its environment variables are available.
 .PP
-The command receives the caller environment.  It also receives
+The command receives the caller environment, plus
 .B PROMPT_ENV_INDICATOR=isolated
 and
 .B ISOLATE_ENV
-set to the resolved working directory.  If
+set to the resolved project directory. If
 .B ISOLATE_ENV
-is already non-empty, pisol warns and runs the command directly.  It does not
-start a nested Landlock domain.
-.SH TERMINAL INTERFACE COMMANDS
+is already set, pisol warns and runs the command directly. It does not start a
+nested Landlock domain.
+.SH TERMINAL COMMANDS
 .TP
 .B a
-Prompt for a base directory path and add a read/write/execute grant.  With no
-input, the picker opens in the user's home directory.  When
-.BR fzf (1)
-is available, the picker lists the typed path and its immediate child
-directories.  Press Enter to open a directory.  Press Alt+Enter to select the
-directory under the cursor.  Press Ctrl-S to save the current directory.  Press
-Escape to use the typed path directly.
+Add a read, write and execute directory grant. The path starts at the home
+directory. Press Tab to complete a path. Press Tab twice to list matches.
 .TP
 .B r
-Prompt for the number of a writable directory and remove it.
+Remove a writable directory by number.
 .TP
 .BI p " PORT"
-Allow outbound TCP connections to PORT.  Valid ports are 1 to 65535.
+Allow outbound TCP to PORT. Valid ports are 1 to 65535.
 .TP
 .B d
-Prompt for a configured outbound TCP port and remove it.
+Remove an outbound TCP port.
 .TP
 .B c
-Show the global init file and local configuration file, including their raw
-contents.  A missing file is labelled absent.
+Show the global init file and local configuration file. A missing file is
+labelled absent.
 .TP
-.B s
-Save atomically with mode 0600.
+.B ?
+Show help.
 .TP
-.BR ? , " x"
-Show help or exit.  Exit discards any unsaved changes.
+.B x
+Save changes atomically with mode 0600, then exit.
 .SH CONFIGURATION
 .I .isolate
-is trusted Bash code.  The launcher sources it.  It changes the Bash array
-.IR args ,
-normally with an
+is trusted Bash code. The launcher sources it. It normally changes the Bash
+array
+.IR args
+with an
 .B args+=(...)
 fragment.
 .PP
@@ -86,51 +82,46 @@ If
 .I ~/.config/pisol/init
 exists, the launcher sources it before the local
 .I .isolate .
-Put shared grants in the global init file.  Per-project configuration builds on
-it.  Set
-.BR PISOL_INIT_CONFIG .
+Use it for shared grants. Set
+.B PISOL_INIT_CONFIG
+to use another init file.
 .PP
-A permissive example global init based on the legacy
-.I isolate
-script is included in the source tree at
+The source tree includes an example init file at
 .IR config/pisol/init .
 Copy it to
 .I ~/.config/pisol/init
-and change it for your needs.  Remove
+and change it for your needs. Remove
 .B --unrestricted-network
-from the copy to deny outbound TCP by default.
+to deny outbound TCP by default.
 .PP
-The editor evaluates an existing file.  It manages the resulting
+pisol evaluates an existing configuration. It manages the resulting
 .B --rwx
 and
 .B --connect-tcp
-pairs and keeps other resulting arguments.  Saving custom shell code requires
-confirmation.  It replaces the code with a static array, so it removes source
-comments and dynamic logic.  The terminal interface shows global init grants.
-It does not let you change them.
+arguments and keeps other arguments. Saving custom shell code replaces it with
+a static array. This removes comments and dynamic logic. The editor shows global
+init grants but does not let you change them.
 .SH ENVIRONMENT
 .TP
 .B ISOLATE_EXTRA_CONFIG
 Override the local configuration path.
 .TP
 .B PISOL_INIT_CONFIG
-Override the global init path.  Defaults to
+Override the global init path. The default is
 .IR ~/.config/pisol/init .
 .TP
 .B ISOLATE_ENV
 Marks an existing pisol sandbox and prevents nesting.
 .TP
 .B NO_COLOR
-Disable TUI colour when set to any value.
+Disable TUI colour when set.
 .TP
 .BR HOME , " PATH" , " TMPDIR"
 Supply runtime paths and grants.
 .TP
 .BR WAYLAND_DISPLAY , " XDG_RUNTIME_DIR"
-Identify an optional Wayland Unix socket grant.
+Identify an optional Wayland socket grant.
 .SH EXIT STATUS
-The terminal interface returns zero on normal exit and non-zero on load or save errors.  In
-command mode, pisol propagates the executed command status.  Usage errors
-return 2 and a missing Landrun executable returns 127.
-.SH SEE ALSO
-.BR fzf (1)
+The editor returns zero on normal exit. It returns non-zero on load or save
+errors. In command mode, pisol returns the command's status. Usage errors return
+2. A missing Landrun executable returns 127.
blob - 141b2d3a14bded8e4f707718e6d773d08d4056c4
blob + deea5a5cd9bc438d38efd8180505449c9f483752
--- src/pisol/tui.sls
+++ src/pisol/tui.sls
@@ -10,7 +10,6 @@
  (define (ansi code text)
    (if color? (string-append esc "[" code "m" text esc "[0m") text))
  (define (cyan text) (ansi "38;2;155;246;255" text))
- (define (green text) (ansi "38;2;202;255;191" text))
  (define (yellow text) (ansi "38;2;253;255;182" text))
  (define (muted text) (ansi "38;2;108;117;125" text))
  (define (intense text) (ansi "1;38;2;248;249;250" text))
@@ -49,106 +48,45 @@
    (let ([line (get-line (current-input-port))])
      (and (not (eof-object? line)) line)))
 
- (define (command-available? command)
-   (zero? (system (string-append "command -v " command " >/dev/null 2>&1"))))
-
  (define (terminal-input?)
    (zero? (system "test -t 0")))
 
  (define (temporary-path)
    (let ([now (current-time)])
-     (format "~a/pisol-fzf-~a.~a"
+     (format "~a/pisol-path-~a.~a"
              (or (getenv "TMPDIR") "/tmp")
              (time-second now)
              (time-nanosecond now))))
 
- (define (read-output-lines path)
-   (and (file-exists? path)
-        (call-with-input-file path
-          (lambda (port)
-            (let loop ([lines '()])
-              (let ([line (get-line port)])
-                (if (eof-object? line)
-                    (reverse lines)
-                    (loop (cons line lines)))))))))
  (define (read-text path)
    (call-with-input-file path get-string-all))
 
- (define (read-current-path path)
-  (and (file-exists? path)
-       (let ([content (read-text path)])
-         (let ([trimmed (trim content)])
-           (if (string=? trimmed "") #f trimmed)))))
-
- (define (fuzzy-directory base)
-   (if (or (not (terminal-input?))
-           (not (command-available? "fzf"))
-           (not (file-directory? base)))
-       base
-       (let* ([output (temporary-path)]
-              [current-file (string-append output ".dir")]
-              [fd? (command-available? "fd")]
-              [child-list (lambda (dir)
-                            (if fd?
-                                (string-append "fd --absolute-path --type directory "
-                                               "--hidden --exclude .git --max-depth 1 . "
-                                               (shell-quote dir) " 2>/dev/null")
-                                (string-append "find " (shell-quote dir)
-                                               " -maxdepth 1 -type d 2>/dev/null")))]
-              [initial (child-list base)]
-              [reload (child-list "{+1}")]
-              [command
-               (string-append
-                "{ " initial "; } "
-                "| fzf --scheme=path --layout=reverse --height=80% --border"
-                " --expect=esc,ctrl-s"
-                " --prompt='Directory path: '"
-                " --header='Enter drills down. Alt+Enter selects the cursor item. Ctrl-S saves the current directory. Esc uses the typed path.'"
-                " --bind 'enter:execute-silent(printf %s '{+1}' > " (shell-quote current-file) ")+reload(" reload ")+clear-query'"
-                " --bind 'alt-enter:accept'"
-                " >" (shell-quote output))])
+ (define (read-directory-path prompt initial)
+   ;; Bash Readline supplies native filename completion on TAB.
+   (if (not (terminal-input?))
+       (read-input prompt)
+       (let ([output (temporary-path)])
          (dynamic-wind
            (lambda () #f)
            (lambda ()
-             (let ([status (system command)])
-               (cond
-                 [(not (zero? status)) #f]
-                 [else
-                  (let ([lines (read-output-lines output)])
-                    (cond
-                      [(not lines) base]
-                      [(null? lines) base]
-                      [(string=? (car lines) "esc") base]
-                      [(string=? (car lines) "ctrl-s") (or (read-current-path current-file) base)]
-                      [else
-                       (let ([selection (car lines)])
-                         (if (not (string=? (trim selection) ""))
-                             selection
-                             base))]))])))
+             (let ([status (system (string-append
+                                    "bash -c "
+                                    (shell-quote "IFS= read -e -r -i \"$3\" -p \"$1\" path || exit $?; printf '%s' \"$path\" > \"$2\"")
+                                    " bash " (shell-quote prompt) " " (shell-quote output) " " (shell-quote initial)))])
+               (and (zero? status) (file-exists? output) (read-text output))))
            (lambda ()
-             (when (file-exists? output) (delete-file output))
-             (when (file-exists? current-file) (delete-file current-file)))))))
+             (when (file-exists? output) (delete-file output)))))))
 
  (define (choose-directory)
    (let* ([home (getenv "HOME")]
-          [default (or home "")]
-          [prompt (if (string=? default "")
-                      "Directory path: "
-                      (string-append "Directory path [" default "]: "))]
-          [entry (read-input prompt)])
-     (and entry
-          (let ([base (trim entry)])
-            (if (string=? base "")
-                (and (not (string=? default ""))
-                     (fuzzy-directory default))
-                (fuzzy-directory base))))))
+          [default (if (and home (file-directory? home))
+                       (if (string=? home "/") home (string-append home "/"))
+                       "")])
+     (let ([entry (read-directory-path "" default)])
+       (and entry
+            (let ([path (trim entry)])
+              (if (string=? path "") default path))))))
 
- (define (yes? prompt)
-   (let ([answer (read-input (string-append prompt " [y/N] "))])
-     (and answer
-          (member (string-downcase (trim answer)) '("y" "yes"))
-          #t)))
-
  (define (integer-text text)
    (and (> (string-length text) 0)
         (for-all char-numeric? (string->list text))
@@ -187,7 +125,7 @@
 
  (define (render configuration status)
    (clear-screen)
-   (display (intense "pisol ยท isolation"))
+   (display (intense "pisol"))
    (newline)
    (display (muted "config:  "))
    (display (configuration-config-path configuration))
@@ -217,13 +155,10 @@
    (newline)
    (display "a add dir     r remove dir     p add port     d remove port")
    (newline)
-   (display "s save        c show config    ? help         x exit")
+   (display "c config      x save and exit")
    (newline)
-   (when (configuration-dirty? configuration)
-     (display (yellow "unsaved changes"))
-     (newline))
-   (when (and status (not (string=? status "")))
-     (display (if (string-prefix? "saved" status) (green status) (yellow status)))
+   (unless (string=? status "")
+     (display (yellow status))
      (newline)))
 
  (define (show-help)
@@ -231,16 +166,16 @@
    (display (intense "pisol help"))
    (newline)
    (newline)
-   (display "Enter a base path; fuzzy selection lists it and its immediate children.")
+   (display "The path starts at your home directory. Press Tab to complete it.")
    (newline)
-   (display "Cancel the picker to use the typed path directly.")
+   (display "Press Tab twice to list matching paths.")
    (newline)
    (display "The baseline writable tree cannot be added again. TCP is denied unless listed.")
    (newline)
    (newline)
-   (display "Existing custom .isolate shell is trusted and evaluated. Saving it replaces")
+   (display "Custom .isolate files are trusted shell code. Saving replaces them with")
    (newline)
-   (display "the source with the canonical evaluated Landrun argument list.")
+   (display "a static list of Landrun arguments.")
    (newline)
    (display "c shows the raw global and local configuration files.")
    (newline)
@@ -260,7 +195,7 @@
                     [(not path) #t]
                     [(string=? (trim path) "") (loop "directory path is required")]
                     [(add-directory! configuration (trim path))
-                     (loop (string-append "added " (trim path)))]
+                     (loop (string-append (trim path) " added"))]
                     [else
                      (loop "directory is missing, duplicated, or already writable")]))]
                [(string=? command "r")
@@ -293,21 +228,15 @@
                         (if (and port (remove-port! configuration port))
                             (loop (format "removed outbound TCP port ~a" port))
                             (loop "configured port not found")))))]
-               [(string=? command "s")
-                (cond
-                  [(not (configuration-dirty? configuration))
-                   (loop "nothing to save")]
-                  [(and (eq? (configuration-source-style configuration) 'custom)
-                        (not (yes? "Replace custom shell with canonical evaluated arguments?")))
-                   (loop "save cancelled")]
-                  [else
-                   (guard (condition
-                           (else (loop (format "save failed: ~a" condition))))
-                     (save-configuration! configuration)
-                     (loop "saved .isolate"))])]
                [(string=? command "?") (show-help) (loop "")]
                [(string=? command "c") (show-config-files configuration) (loop "")]
-               [(string=? command "x") #t]
+               [(string=? command "x")
+                (if (not (configuration-dirty? configuration))
+                    #t
+                    (guard (condition
+                            (else (loop (format "save failed: ~a" condition))))
+                      (save-configuration! configuration)
+                      #t))]
                [(string=? command "") (loop status)]
                [else (loop "unknown command; enter ? for help")]))))))
 )
blob - 09a680f38add099be98173fb824792979f375e2a
blob + 3a6505499fbcbbb348dac2226dc9734c39f81d9c
--- test/integration.sh
+++ test/integration.sh
@@ -2,7 +2,8 @@
 set -eu
 
 project_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
-test_root=${TMPDIR:-/tmp}/pisol-integration.$$
+outer_isolate_env=${ISOLATE_ENV:-}
+test_root=$(mktemp -d "$project_root/.pisol-integration.XXXXXX")
 fake_bin=$test_root/bin
 project=$test_root/project
 home=$test_root/home
@@ -24,6 +25,9 @@ chmod +x "$fake_bin/landrun"
 run_pisol() {
   (
     cd "$project"
+    if [ "${ISOLATE_ENV:-}" = "$outer_isolate_env" ]; then
+      unset ISOLATE_ENV
+    fi
     PATH=$fake_bin:$PATH \
     HOME=$home \
     PISOL_RUNTIME_DIR=$project_root/build/runtime \
@@ -59,7 +63,7 @@ grep -Fx -- '443' "$test_root/args.txt" >/dev/null
 grep -Fx -- "$home/extra dir" "$test_root/args.txt" >/dev/null
 
 before=$(cksum "$capture")
-if ISOLATE_ENV=already run_pisol -- sh -c 'exit 9'; then
+if ISOLATE_ENV=already run_pisol -- sh -c 'exit 9' 2>/dev/null; then
   exit 1
 else
   status=$?
@@ -71,10 +75,10 @@ run_pisol --help | grep -F 'pisol COMMAND' >/dev/null
 run_pisol --help | grep -F 'pisol -- COMMAND' >/dev/null
 
 rm -f "$project/.isolate"
-printf 'a\n%s\np 443\ns\nx\n' "$home/extra dir" | run_pisol >"$test_root/tui.out"
+printf 'a\n%s\np 443\nx\n' "$home/extra dir" | run_pisol >"$test_root/tui.out"
 grep -F -- "--rwx '$home/extra dir'" "$project/.isolate" >/dev/null
 grep -F -- '--connect-tcp 443' "$project/.isolate" >/dev/null
-grep -F 'saved' "$test_root/tui.out" >/dev/null
+grep -F 'x save and exit' "$test_root/tui.out" >/dev/null
 grep -F 'r remove dir' "$test_root/tui.out" >/dev/null
 grep -F 'd remove port' "$test_root/tui.out" >/dev/null
 if grep -E 'r NUMBER remove dir|d PORT remove' "$test_root/tui.out" >/dev/null; then
@@ -86,7 +90,7 @@ if grep -E 'project:|p project|h home|browse/add' "$te
   exit 1
 fi
 
-printf 'r\n1\nd\n443\ns\nx\n' | run_pisol >"$test_root/remove.out"
+printf 'r\n1\nd\n443\nx\n' | run_pisol >"$test_root/remove.out"
 if grep -F -- '--rwx' "$project/.isolate" >/dev/null ||
    grep -F -- '--connect-tcp' "$project/.isolate" >/dev/null; then
   echo "pisol integration: prompted removal did not remove configuration" >&2
@@ -98,9 +102,13 @@ printf 'p 8443\n' | run_pisol >/dev/null
 [ "$saved" = "$(cksum "$project/.isolate")" ]
 
 printf '%s\n' 'if true; then args+=(--ro /custom); fi' >"$project/.isolate"
-custom=$(cksum "$project/.isolate")
-printf 'p 80\ns\nn\nx\ny\n' | run_pisol >/dev/null
-[ "$custom" = "$(cksum "$project/.isolate")" ]
+printf 'p 80\nx\n' | run_pisol >/dev/null
+grep -F -- '/custom' "$project/.isolate" >/dev/null
+grep -F -- '--connect-tcp 80' "$project/.isolate" >/dev/null
+if grep -F 'if true' "$project/.isolate" >/dev/null; then
+  echo "pisol integration: custom configuration was not canonicalized on exit" >&2
+  exit 1
+fi
 
 # Global init defaults are sourced before the local .isolate.
 global_init=$home/.config/pisol/init
@@ -120,7 +128,7 @@ grep -Fx -- '443' "$test_root/args.txt" >/dev/null
 
 # show-config command prints both global and local configuration files.
 printf 'c\n\nx\n' | run_pisol >"$test_root/showconfig.out"
-grep -F 'c show config' "$test_root/showconfig.out" >/dev/null
+grep -F 'c config' "$test_root/showconfig.out" >/dev/null
 grep -F -- "$global_init" "$test_root/showconfig.out" >/dev/null
 grep -F -- "$project/.isolate" "$test_root/showconfig.out" >/dev/null
 grep -F -- "$home/extra dir" "$test_root/showconfig.out" >/dev/null