A working prototype of an interactive Bash shell that intercepts /cmd args
input at the readline layer and routes it to handler functions, while
leaving normal shell operation untouched. Single-machine dev demo on the
okusi workstation. Not packaged; not deployed.
[leet] $ ls /tmp # ← normal bash
[leet] $ /agent claude # ← slash command (intercepted)
agent set to: claude
[claude] $ /ask 'one-line haiku about bash'
...
Initialise a bash shell sourced with functions that also run AI LLM commands. The shell should operate normally, except when a
/char appears as the first char of a command line; when this happens, a dispatcher function should be called to see if it is a slash command:/ask /model /knowledgebase /...
Bash treats any token whose first char is / as an absolute path and runs
execve() immediately. The command_not_found_handle hook only fires
after execve fails — by which point the user has already seen
bash: /foo: No such file or directory. The earlier prototype
.gudang/process-llm-command used that hook and is preserved as a
historical record of an unsalvageable approach. Do not revive it.
slash-bash intercepts the line at the readline layer, before bash parses it for execution. Two key bindings cooperate:
| Binding | Effect |
|---|---|
\C-xs (bind -x) |
Runs __sb_intercept in the live shell. Reads READLINE_LINE; if it begins with /, rewrites it to __sb_dispatch '<original>' using ${line@Q} for safe quoting. |
\C-m (Enter) |
Rebound to the macro \C-xs\C-j — interceptor first, then \C-j (the still-default accept-line) submits whatever the line now contains. |
By the time bash parses the line, it sees a normal function call rather
than a leading /. Lines that don't start with / pass through unchanged.
The ${line@Q} quoting is the security boundary — it makes the dispatcher
immune to command injection via crafted input like /foo'; rm -rf ~.
bash-preexec.sh (vendored) provides preexec_functions so dispatched
commands can be logged to ~/.cache/slash-bash/history without
re-implementing fragile DEBUG-trap plumbing.
cd <slash-bash repo>
sudo symlink -S . # exposes the launcher to /usr/local/bin
slash-bash # opens an isolated interactive bashThe launcher execs bash --init-file .slash-bash-init -i, so your
normal ~/.bashrc is inherited but your outer shell is untouched. The
PS1 ends with ╱ <agent-name> and updates live when you /agent, so
it is always obvious which agent is active. Examples below use the
shorthand [leet] $ for readability — the actual rendered prompt is
multi-line and includes user@host, cwd, and the agent name after a
╱ separator.
slash-bash --version # 1.0.0
slash-bash --help| Command | Effect |
|---|---|
/help |
Full inline help with per-command descriptions. |
/? |
Short help (one line per command). |
/agents [--select|--list] |
--select (default) presents a numbered picker via the bash select builtin and sets _SB_AGENT on choice; q/empty/Ctrl-D cancels. --list prints the legacy listing. Lazy-loads agents from claude.agent --list on first use. |
/agent <name> |
Set the active agent (case-insensitive exact match, then prefix match). No arg shows current. |
/models [--select|--list] |
--select (default) presents a numbered picker via the bash select builtin and sets _SB_MODEL on choice. --list prints the legacy listing. |
/model [name] |
No arg: show current model. With arg: set it. |
/kbs, /knowledgebases [--select|--list] |
--select (default) presents a numbered picker via the bash select builtin and sets _SB_KB on choice. --list prints the legacy listing. Both report "no knowledgebases found under …" if the root is empty. |
/kb <name>, /knowledgebase <name> |
Switch active KB (validated against /var/lib/vectordbs/<name>/<name>.cfg). off/none/clear to clear. No arg shows current. |
/maxtokens <n> |
Set CLAUDE_CODE_MAX_OUTPUT_TOKENS for subsequent /ask calls. No arg shows current. Positive integer. |
/ollama [model|off] |
Toggle Ollama backend. Bare /ollama enables with default model; <model> enables and exports OLLAMA_MODEL; off disables. |
/ask <text>, / <text> |
One-shot LLM call: claude.agent -T <agent> --model <model> [-O] [--append-system-prompt-file <kb-cfg>] <text> with CLAUDE_CODE_MAX_OUTPUT_TOKENS set. |
/systemprompt [agent] |
Show the system prompt for the active or named agent. Reads Agents.json (override path with AGENTS_JSON env var). Substitutes {{spacetime}} so the displayed prompt mirrors what the model actually sees. |
/history [n] |
Show the last n slash invocations from ~/.cache/slash-bash/history (default 20). |
/status |
Print a bash-reusable env dump of current session state — one export NAME='value' line per SB_* knob (and the cross-project VECTORDBS, AGENTS_JSON, OLLAMA_MODEL, SB_CLAUDE_PROJECTS_DIR). Output is eval-safe; redirect to a file and source it before re-launching slash-bash to round-trip a saved session. |
/sessions |
List Claude Code sessions for the current cwd as TSV (mtime, uuid, title), newest first. UUIDs are accepted by claude --resume. Reads ~/.claude/projects/<encoded-cwd>/*.jsonl. Thin wrapper that delegates to the standalone claude-sessions script (also available as a top-level command in any shell). |
/rebase [--force] |
Bind the active conversation to $PWD's most-recent JSONL session (auto-pick — most recent by mtime). Subsequent /ask calls run inside the bound cwd and resume that UUID via claude.agent --resume <uuid>, regardless of where the user has cd'd. Default binding is the cwd at slash-bash entry; _SB_SESSION_UUID is empty until first /ask adopts the freshly-minted JSONL. If the current binding has recent /ask activity (within SB_DIRTY_WINDOW seconds — default 300), prompts before discarding; --force or non-TTY caller skips the prompt. No prior JSONL in the new cwd → next /ask starts a fresh conversation. |
/filemanager [<dir>], /fm |
Launch a file manager on <dir> (default $PWD). Auto-detects mode: GUI (xdg-open, detached) when $DISPLAY/$WAYLAND_DISPLAY is set, else TUI fallback (probes $SB_FILEMANAGER, mc, ranger, nnn, lf, vifm; foreground). Flags: -g/--gui and -t/--tui force the mode; -n/--dry-run prints what would launch. |
/version |
Print slash-bash version. |
/exit |
Leave slash-bash. |
Anything not starting with / is plain bash:
[leet] $ ls /tmp
[leet] $ for i in 1 2 3; do echo "$i"; done
[leet] $ echo hello | wc -c
Anything that starts with / but isn't a known slash command prints
unknown slash command: '/foo' instead of the bash No such file or directory error. The interceptor claims all lines that start with / —
there is no fallthrough to bash. This matches the CLAUDE.md goal of the
dispatcher being the single owner of /-prefixed input.
First-word completion is wired via complete -I (Bash 5.0+, in this
project Bash 5.2+), which registers an "initial-word" compspec — the only
way to override bash's built-in pathname completion for /-prefixed words.
The completer (_sb_complete_first) reads keys from the _SB_HANDLERS
registry for tokens starting with /, and falls through to compgen -c
otherwise so non-slash commands tab-complete normally. _SB_SLASH_CMDS
is auto-derived from ${!_SB_HANDLERS[@]} at end-of-init for legacy
site-hook compatibility but is not the source of truth.
Per-command argument completers are registered with the standard
complete -F path:
| Command(s) | Completer | Source |
|---|---|---|
/agent, /systemprompt |
_sb_complete_agent |
claude.agent --list (lazy) |
/model |
_sb_complete_model |
_SB_MODEL_LIST |
/kb, /knowledgebase |
_sb_complete_kb |
_SB_KB_LIST plus off/none/clear |
/ollama |
_sb_complete_ollama |
off/0/false |
/rebase |
_sb_complete_rebase |
--force |
slash-bash reads its initial state and filesystem layout from environment
variables. Every variable has a default that reproduces the workstation
layout on okusi, so unsetting all of them yields the original behaviour.
| Variable | Default | Effect |
|---|---|---|
SB_AGENT |
leet |
Initial active agent. Overrides the library's default. |
SB_MODEL |
claude-opus-4-7 |
Initial active model. |
SB_MODEL_LIST |
claude-opus-4-7 claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-6 |
Space-separated menu shown by /models and /model tab-completion. |
SB_KB_LIST |
(unset → discovered from $VECTORDBS) |
Space-separated KB menu. Overrides discovery for a curated list. |
SB_MAX_TOKENS |
32000 |
Initial CLAUDE_CODE_MAX_OUTPUT_TOKENS for /ask calls. |
SB_OLLAMA |
0 |
Initial Ollama-on flag (1 = on at start). |
SB_DIRTY_WINDOW |
300 |
Seconds since last /ask within which /rebase prompts before discarding the bound conversation. 0 disables the prompt. |
SB_SESSION_CWD |
$PWD at source time |
Initial conversation-binding cwd. Restore path for /status round-trip. |
SB_SESSION_UUID |
(empty — adopted on first /ask) |
Initial conversation-binding JSONL UUID. Restore path for /status round-trip. |
SB_SESSION_LAST_ASK |
0 |
Initial EPOCHSECONDS of last /ask; gates /rebase dirty-prompt. Non-integer values coerce to 0 via declare -ig. Restore path for /status round-trip. |
| Variable | Default | Effect |
|---|---|---|
VECTORDBS (Okusi-wide convention) |
/var/lib/vectordbs |
Filesystem root holding <name>/<name>.cfg per KB. Read by /kb, /kbs, /ask. |
SB_HISTORY_FILE |
${XDG_CACHE_HOME:-$HOME/.cache}/slash-bash/history |
Slash-invocation log path. Parent dir is auto-created. |
SB_CLAUDE_PROJECTS_DIR |
$HOME/.claude/projects |
Where Claude Code stores per-cwd JSONL sessions. Read by /sessions and claude-sessions. |
SB_SITE_BASH |
${XDG_CONFIG_HOME:-$HOME/.config}/slash-bash/site.bash |
Optional per-site extension file sourced at end of init. See How to extend. |
AGENTS_JSON |
derived from claude.agent in PATH |
Path to the agent registry. Read by /systemprompt. |
SB_FILEMANAGER |
(unset; uses default probe order) | Preferred TUI file manager binary; tried before mc, ranger, nnn, lf, vifm. Read by /filemanager. |
| Variable | Read by | Effect |
|---|---|---|
OLLAMA_MODEL |
/ollama, /ask |
Set by /ollama <model>; consumed by claude.agent -O. |
OLLAMA_HOST |
Ollama client | Honoured directly by Ollama; unused by slash-bash code. Set to http://gpu-box:11434 etc. for a remote daemon. |
CLAUDE_CODE_MAX_OUTPUT_TOKENS |
/ask |
Set per-invocation from _SB_MAX_TOKENS. |
TZ |
/systemprompt |
Used to render the {{spacetime}} substitution. Falls back to /etc/timezone, then UTC. |
DEBUG |
library load time | If non-empty, the rewritten __sb_dispatch … form is not hidden from history (useful when debugging the chord trick). |
HISTIGNORE |
library load time | Patched (idempotently) to suppress __sb_dispatch * from history when DEBUG is unset. |
SB_VERBOSE |
library load time | Initial value of the internal _VERBOSE gate (0/1); _info/_success/_vecho are no-ops unless set. |
SB_DEBUG |
library load time | Initial value of the internal _DEBUG gate; _debug messages are emitted when non-zero. Distinct from DEBUG (which only governs HISTIGNORE). |
SB_LOG_ARGS |
history logger | If non-empty, the full original /cmd args… line is recorded in $_SB_HISTORY_FILE. Default logs only the verb (e.g. /ask) so prompt text and PII never land on disk. |
XDG_CACHE_HOME, XDG_CONFIG_HOME |
library load time | Standard XDG base-dir overrides for the history file and site hook defaults. |
slash-bash ships with Okusi-workstation defaults but every machine-specific path is overridable. Three recipes:
Point VECTORDBS at a user-owned directory and drop a <name>/<name>.cfg
underneath:
export VECTORDBS=$HOME/kb
mkdir -p "$VECTORDBS/mykb"
printf 'You are a helpful assistant focused on X.\n' > "$VECTORDBS/mykb/mykb.cfg"
slash-bash
[leet] $ /kbs # mykb appears
[leet] $ /kb mykbThe VECTORDBS env-var is the cross-project Okusi convention also honoured
by claude.agent and the vectordbs tooling — same redirect works
everywhere.
No code change. The Ollama client reads OLLAMA_HOST directly:
export OLLAMA_HOST=http://gpu-box:11434
slash-bash
[leet] $ /ollama llama3:70bexport AGENTS_JSON=$HOME/.config/agents.json
slash-bash
[leet] $ /agentscat >>~/.bashrc <<'EOF'
export VECTORDBS=$HOME/kb
export AGENTS_JSON=$HOME/.config/agents.json
export SB_HISTORY_FILE=$HOME/.cache/slash-bash/history
export SB_SITE_BASH=$HOME/.config/slash-bash/site.bash
EOFAfter that, the only system path slash-bash touches is the working directory itself.
| File | Purpose |
|---|---|
slash-bash |
Launcher — execs an isolated bash (bash --init-file .slash-bash-init -i). |
.slash-bash-init |
rcfile companion — sources ~/.bashrc, then the library, then sets PS1 to embed the active agent name. |
slash-bash.bash |
Sourceable library — interceptor, dispatcher, handlers, completers, key bindings. |
claude-sessions |
Standalone CLI to list Claude Code sessions for the current cwd. Exposed via .symlink. The slash-bash /sessions handler delegates to it. |
bash-preexec.sh |
Vendored upstream (MIT) — preexec/precmd hooks. |
.symlink |
Maps the launcher and claude-sessions for symlink -S exposure. |
BASH-CODING-STANDARD.md |
Symlink → /usr/local/share/yatti/BCS/data/BASH-CODING-STANDARD.md. |
.gudang/process-llm-command |
Historical record of approach A (abandoned). |
.gitignore keeps CLAUDE.md, .claude/, the BCS symlink, and any
AUDIT-*.md out of version control per enterprise policy.
| Dependency | Min version | Source / project | Notes |
|---|---|---|---|
| Bash | 5.2+ | https://www.gnu.org/software/bash/ | Chord trick uses bind -x and \C-m macro rebinding. complete -I requires 5.0+. ${var@Q} requires 4.4+. We target 5.2+ exclusively. |
| coreutils | recent | https://www.gnu.org/software/coreutils/ | realpath, mkdir -p, sort, head, tail, cut, date -r, printf. date -Iseconds is GNU-only but _sb_iso_now falls back to a portable date +'%Y-%m-%dT%H:%M:%S%z' invocation. date -r FILE FORMAT works on both GNU and BSD. |
| awk | any POSIX | https://www.gnu.org/software/gawk/ | Strips column 1 from claude.agent --list. |
| sed | any POSIX | https://www.gnu.org/software/sed/ | Strips ANSI color escapes from claude.agent --list output. |
| File | Project | License | URL |
|---|---|---|---|
bash-preexec.sh |
bash-preexec by @rcaloras (BCS-adapted copy) | MIT | https://github.com/rcaloras/bash-preexec |
| Dependency | Used by | Source / project | Behaviour if absent |
|---|---|---|---|
jq |
/systemprompt, /sessions |
https://github.com/jqlang/jq | /systemprompt prints jq not in PATH and returns 1. /sessions delegates to claude-sessions, which prints the same message and returns 18 (BCS ERR_NODEP). |
claude.agent |
/ask, /agents, /agent, /systemprompt |
Okusi internal — must be in PATH (alongside its Agents.json registry) |
Those commands print claude.agent not in PATH; the rest of the shell still works. |
claude-sessions |
/sessions |
This project (sibling script in the slash-bash dir, also installed to PATH via .symlink) |
/sessions prints claude-sessions not found or not executable and returns 18. |
Claude Code CLI (claude) |
/sessions (UUIDs are passed to claude --resume) |
https://github.com/anthropics/claude-code | /sessions only reads ~/.claude/projects/<…>/*.jsonl; running the CLI is left to the user. |
| Ollama | /ollama, /ask -O |
https://github.com/ollama/ollama | Only invoked indirectly by claude.agent -O. Toggled-on but missing → claude.agent will report it. |
symlink (Okusi tool) |
install step | Okusi internal — /usr/local/bin/symlink |
Replace with manual ln -s if absent. |
| Tool | Source | URL | Notes |
|---|---|---|---|
| ShellCheck | shellcheck | https://github.com/koalaman/shellcheck | shellcheck -x is mandatory and currently passes clean on every in-tree bash file: slash-bash, slash-bash.bash, .slash-bash-init, claude-sessions, plus every handlers.d/_*.bash. |
| bcscheck | Okusi BCS toolchain | (internal) | LLM-backed BCS compliance check. ~10 minutes per script. Optional. |
| BCS data | Bash Coding Standard | (internal — symlinked) | /usr/local/share/yatti/BCS/data/. |
Adding a /cmd is a single new file under handlers.d/. The
library globs handlers.d/_*.bash at init time; each handler file
defines its body, optional argument completer, and registers itself
into the _SB_HANDLERS / _SB_HELP / _SB_COMPLETE registry maps.
Worked example — drop this at handlers.d/_weather.bash:
#!/usr/bin/bash
#shellcheck shell=bash disable=SC2034
# handlers.d/_weather.bash - the /weather handler.
_sb_cmd_weather() {
local -- arg=$*
printf 'mock weather report (location: %s)\n' "${arg:-here}"
}
# Optional: argument completer. Omit if the command takes no args.
_sb_complete_weather() {
local -- cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=()
readarray -t COMPREPLY < <(compgen -W 'jakarta sydney london' -- "$cur")
}
# Registration (canonical position: end of file).
_SB_HANDLERS[/weather]=_sb_cmd_weather
_SB_HELP[/weather]='show mock weather for a city'
_SB_COMPLETE[/weather]=_sb_complete_weather
#finThat's it. The dispatcher reads _SB_HANDLERS at runtime; _SB_SLASH_CMDS
is auto-derived from ${!_SB_HANDLERS[@]} so first-word TAB completion
picks up the new command without any further edit. Aliases are extra keys
on _SB_HANDLERS (e.g. _SB_HANDLERS[/w]=_sb_cmd_weather).
If you don't want to commit a handler upstream, drop a file at
${XDG_CONFIG_HOME:-$HOME/.config}/slash-bash/site.bash (or set
SB_SITE_BASH to point elsewhere). It's sourced at the end of the
library load, after handlers.d/*.bash and after key bindings /
compspecs are registered.
The site hook is gated by an ssh-style ownership/permissions check: it
will be skipped (with a warning) if it is not owned by $USER or if
group/other write bits are set (mode 0?22 or worse). This prevents the
classic shared-box confused-deputy trap where a careless chmod -R g+w
under $HOME would let any group member inject code into every
slash-bash session. Keep the file chmod 600 (or 644).
Prefer the registry path:
# ~/.config/slash-bash/site.bash
_sb_cmd_ticket() {
local -- arg=$*
[[ -z $arg ]] && { >&2 printf 'usage: /ticket <text>\n'; return 1; }
printf 'creating Linear ticket: %s\n' "$arg"
# … your linear-cli call here …
}
_SB_HANDLERS[/ticket]=_sb_cmd_ticket
_SB_HELP[/ticket]='create a Linear ticket from the current shell'
# Override default agent for this host:
_SB_AGENT=claudeLegacy: _SB_SLASH_CMDS+=(/ticket) continues to work for completion-only
entries (no handler bound; dispatch will report "unknown slash command"
when invoked). The registry path is preferred for all new commands.
Future hooks (still out of scope):
- Streaming output via
coprocso the prompt stays responsive during long LLM calls. - Persistent state (active agent, model, KB) in
~/.config/slash-bash/state. - Multi-line input via PS2-aware intercept.
shellcheck -x slash-bash slash-bash.bash .slash-bash-init claude-sessions handlers.d/*.bash
bcscheck slash-bash.bash # full BCS, LLM-backed, slow (~10 min)
bcscheck claude-sessions # ditto
make test # bats suite (266 cases)
make test-e2e # PTY-driven chord-trick verification
make check # combined lint + test gateFunctional smoke test (must run under a real TTY — the chord trick won't fire over piped stdin):
slash-bash
[leet] $ /help
[leet] $ /agents
[leet] $ /agent leet
[leet] $ /maxtokens 16000
[leet] $ /history
[leet] $ /exit- Single-line input only — multi-line dispatch is not handled.
/ask,/agents,/agent,/systempromptrequireclaude.agentinPATH. The agent list is loaded lazily on first use, so other commands still work without it.- KB validation is path-based: a name is accepted only if
$VECTORDBS/<name>/<name>.cfgexists (default/var/lib/vectordbs)./kbsdiscovers KB names by globbing that root; override the menu withSB_KB_LIST="kb1 kb2"or change the root withVECTORDBS=…. - The interceptor claims all lines that start with
/. There is no fallthrough to bash for unknown slash commands — by design (see Goal). If fallthrough is wanted, change the*)arm in__sb_dispatch(aneval -- "$line"would do it, with the obvious caveats). - emacs and vi-insert keymaps are bound; vi-command keymap is not.
/sessionsencodes the cwd by replacing/with-, which is ambiguous (/foo-barand/foo/barcollide). This mirrors Claude Code's own scheme and must stay in sync with it.
- BCS (Bash Coding Standard) compliance: ~92%. The library deliberately
omits
set -euo pipefailbecause errexit in a sourced library would kill the live interactive shell on any handler failure; this is documented in the source. Runmake audit(gitignored output →AUDIT-BASH.md) to reproduce the findings locally. - ShellCheck: clean (
-x) on every in-tree bash file (launcher, library, init rcfile,claude-sessions, and everyhandlers.d/_*.bash). - Enterprise policy:
CLAUDE.mdand.claude/never committed (enforced by.gitignore).
#fin