Custom Commands
Custom commands extend Swival with personal scripts and prompt templates stored in your commands directory. Type !name and Swival resolves name to a file in ~/.config/swival/commands/, then either runs it and injects its stdout, or inlines its text content directly into the conversation.
Not to be confused with !! <command> (quick shell), which runs an arbitrary shell command and prints the output without any LLM involvement. See the Input Commands section for details.
In interactive mode, custom commands are always available. In one-shot mode, they require --oneshot-commands because the input may come from an untrusted source:
swival --oneshot-commands "!context"
Setup
Create the commands directory:
mkdir -p ~/.config/swival/commands
If you set XDG_CONFIG_HOME, the directory is $XDG_CONFIG_HOME/swival/commands instead.
A file in this directory is a valid command if its name (or stem, ignoring extension) contains only letters, digits, hyphens, and underscores, and the file is not a dot-file or backup artefact (names starting with ., ending with ~, or with extensions .bak, .orig, .swp, .swo, .tmp, .pyc are silently skipped).
There are two kinds of commands:
Executable scripts
An executable script is run as a subprocess, and its stdout is injected as the next user message. Any language with a shebang line works.
cat > ~/.config/swival/commands/context <<'EOF'
#!/bin/sh
base_dir="$1"
cd "$base_dir"
echo "## Git status"
git status --short
echo "## Recent commits"
git log --oneline -5
EOF
chmod +x ~/.config/swival/commands/context
Text templates
A plain text file without the execute bit is treated as a prompt template. Its content is read and inlined directly into the conversation — no subprocess is started. The file must be valid UTF-8 and contain no null bytes (binary files are rejected with "command not executable").
cat > ~/.config/swival/commands/review <<'EOF'
Review the changes in $1 for correctness, style, and test coverage. Flag any security issues first.
EOF
No chmod +x is needed. The file is inlined as-is, with leading and trailing whitespace preserved.
Usage
Prefix the command name with !:
swival> !context
swival> !review main branch
Everything after the command name is the argument string. How it is used depends on the command type (see below).
Resolution and Precedence
Swival scans the commands directory for all eligible files whose name or stem matches the command name (case-insensitive on Windows), then picks the best match using this priority order:
- Exact-name executable (
commands/namewith execute bit) - Stem-match executable (
commands/name.sh,commands/name.py, …) - Exact-name text template (
commands/namewithout execute bit) - Stem-match text template (
commands/name.md,commands/name.txt, …)
Executables always win over text templates. If more than one file ties within a tier, Swival reports an ambiguity error. On Unix, extensionless files take the exact-name slots; on Windows, name scripts with an extension (greet.bat, greet.cmd) and Swival resolves them via the stem.
Argument Passing
Executable scripts
The script receives the project root as $1. If arguments are provided after the command name, the raw argument string is passed as $2. If no arguments are given, only $1 is passed. The working directory is also set to the project root.
$COMMANDS_DIR/name $base_dir "$args"
So !deploy staging --dry-run sets $1 to the project root and $2 to staging --dry-run.
Text templates
For text templates there is no subprocess, so $1 is not reserved for the project root. Instead, $1 and $@ in the file content are both replaced with the argument string verbatim before injection. If no arguments are given, $1 and $@ are left as-is in the content.
# commands/review (no execute bit)
Review the changes in $1 for correctness and test coverage.
# !review src/auth.py → "Review the changes in src/auth.py for correctness and test coverage."
Note the difference from executable commands: if you convert a text template into a shell script, rename $1 to $2 and add the base_dir parameter.
Environment Variables
Executable scripts inherit the parent environment. Swival also sets:
| Variable | Description |
|---|---|
SWIVAL_MODEL |
The resolved model identifier for the current session (when available) |
Environment variables are not relevant for text templates since no process is spawned.
Output Handling
For executable scripts, the command's stdout is stripped, printed to the terminal for review, and injected as a user message. For text templates, the content is inlined silently with a brief status line ([!name] inline: ~/path). Both paths respect the context window: if the content is too large for the remaining window, it is truncated to fit. When the context window size is unknown, a hard cap of 100KB applies.
A whitespace-only result (after stripping) is treated as no output and skipped.
For executable scripts, stderr is printed to the terminal on success. On failure (non-zero exit), Swival prints a single error message using stderr, stdout, or the exit code (in that priority order) and injects nothing.
Timeouts
Executable scripts have a 30-second timeout. Text templates have no timeout since no process is spawned.
Community Commands
The swival-commands repository has a growing collection of ready-to-use commands contributed by the community, including a full-repo security audit, a C-specific vulnerability scanner, and a pull request reviewer. Installation is just copying files into your commands directory.
If you have built a command that others might find useful, contributions are welcome there.
History
Custom command output is logged to .swival/HISTORY.md with the label [!name] !name [args...] so you can distinguish command-driven turns from typed input.