Skip to content

Repository files navigation

#+title: guard-sh
#+author: Berdan Akyürek

A shell safety layer powered by LLMs. Every command you type is checked before it runs — risky ones require confirmation.

#+begin_example
$ rm -rf /var/log/*
guard-sh: Deletes all files in /var/log recursively. Are you sure? [Y/n]
#+end_example

* How it works
  :PROPERTIES:
  :CUSTOM_ID: how-it-works
  :END:

1. You press Enter on a command.
2. guard-sh intercepts it and sends it to an LLM.
3. If the LLM considers it safe, the command runs normally.
4. If not, you see a one-line warning and a ~[Y/n]~ prompt.
5. Press ~Y~ to proceed, anything else to cancel.

Commands in your *whitelist* skip the LLM entirely.

* Supported LLMs
  :PROPERTIES:
  :CUSTOM_ID: supported-llms
  :END:

  - Gemini (free tier available at [[https://aistudio.google.com]])
  - DeepSeek ([[https://platform.deepseek.com]])
  - OpenAI ([[https://platform.openai.com]])
  - Claude ([[https://console.anthropic.com]])
  - Ollama (local, no API key required — [[https://ollama.com]])

* Requirements
  :PROPERTIES:
  :CUSTOM_ID: requirements
  :END:

- Go 1.22+
- bash, zsh, or fish
- API key(s) for one or more of the cloud LLMs listed above, *or* a running [[https://ollama.com][Ollama]] instance for fully local operation.

* Installation
  :PROPERTIES:
  :CUSTOM_ID: installation
  :END:

** From source

#+begin_src bash
git clone https://github.com/berdanakyurek/guard-sh.git
cd guard-sh
bash install.sh
#+end_src

This will:
- Build the ~guard-sh~ binary and install it to =~/.local/bin/=
- Copy the default config to =~/.config/guard-sh/config.yaml=
- Copy the default prompt to =~/.config/guard-sh/prompt.txt=
- Add shell integration to the rc file of your *active* shell (bash → =.bashrc=, zsh → =.zshrc=, fish → =config.fish=)

** From binary

Download the binary for your platform from the [[https://github.com/berdanakyurek/guard-sh/releases][releases page]], place it in =~/.local/bin/=, then run:

#+begin_src bash
chmod +x ~/.local/bin/guard-sh
guard-sh setup
#+end_src

This will create the config dir, write the shell integration scripts, and add them to the rc file of your active shell.

** Install without shell integration

#+begin_src bash
bash install.sh --without-shell
#+end_src

** Multiple shells

~install.sh~ and ~guard-sh setup~ only add integration for your *active shell* (detected via the parent process). If you use multiple shells, run the command once from each:

#+begin_src bash
# In bash
bash install.sh

# In zsh
bash install.sh

# In fish
bash install.sh
#+end_src

Or use ~guard-sh setup~ after installing the binary:

#+begin_src bash
# In bash
guard-sh setup

# Switch to zsh, then:
guard-sh setup
#+end_src

* Uninstallation
  :PROPERTIES:
  :CUSTOM_ID: uninstallation
  :END:

** Using the shell script

#+begin_src bash
bash uninstall.sh
#+end_src

This removes the binary, shell integration from all rc files, and the config dir.

To keep your config (providers, whitelist, cache):

#+begin_src bash
bash uninstall.sh --keep-config
#+end_src

** Using the built-in command

#+begin_src bash
guard-sh uninstall          # removes shell integration and shell scripts, keeps config
guard-sh uninstall --purge  # also removes the config dir (~/.config/guard-sh)
#+end_src

After running ~guard-sh uninstall~, the binary itself is not removed (a running program cannot delete itself reliably). The command prints the path to remove manually:

#+begin_example
  next  remove the binary manually:
        rm ~/.local/bin/guard-sh
#+end_example

* Configuration
  :PROPERTIES:
  :CUSTOM_ID: configuration
  :END:

Config file: =~/.config/guard-sh/config.yaml=
- Providers can be added interactively with =guard-sh provider add= — see [[#providers][Providers]].
- The whitelist can also be managed with =guard-sh whitelist= commands — see [[#whitelist][Whitelist]].
- Cache settings can also be managed with =guard-sh cache= — see [[#cache][Cache]].

Or you can edit the config file manually.


An example config file is as follows:

#+begin_src yaml
# Providers are tried in order. If one fails, the next is used.
provider_order:
  - ollama
  - gemini
  - deepseek
  - openai
  - claude

providers:
  ollama:
    host: http://localhost:11434/api/chat # optional, this is the default
    model: llama3.2                       # optional, this is the default
  gemini:
    api_key: YOUR_GEMINI_KEY
    model: gemini-3.1-flash-lite-preview  # optional, this is the default
  deepseek:
    api_key: YOUR_DEEPSEEK_KEY
    model: deepseek-chat                  # optional, this is the default
  openai:
    api_key: YOUR_OPENAI_KEY
    model: gpt-4o-mini                    # optional, this is the default
  claude:
    api_key: YOUR_ANTHROPIC_KEY
    model: claude-haiku-4-5-20251001      # optional, this is the default

# Commands always considered safe — LLM is never called.
# Only the base command name is needed: "ls" covers "ls -la", "ls /tmp", etc.
# For chained commands (&&, ||, ;, |), ALL parts must be whitelisted.
command_whitelist:
  - ls
  - cat
  - echo
  - pwd
  - cd
  - clear

# How long to wait for an LLM response, in seconds. Defaults to 10 if not set.
timeout_seconds: 10

# Include the working directory in the command sent to the LLM.
# Allows the LLM to assess risk based on context (e.g. chmod in /etc vs ~/projects).
# Cache keys include the working directory when this is enabled.
# Set to false to send only the command string.
send_working_directory: true

# Cache LLM responses to avoid repeated API calls for the same command.
cache_enabled: true
cache_max_size: 1000
#+end_src

** Custom prompt

The LLM prompt is stored at =~/.config/guard-sh/prompt.txt=. Edit it to adjust what guard-sh considers risky, change the output format, or add your own rules. Changes take effect immediately — no rebuild needed.

** Working directory context

By default, guard-sh includes the current working directory when querying the LLM. This gives the LLM context to make better decisions — for example, =chmod 777 config.yaml= in =/etc= is riskier than in =~/projects/=.

The LLM receives a structured two-line query:

#+begin_example
Working directory: /etc
Command: chmod 777 config.yaml
#+end_example

The working directory is also included in cache keys, so the same command in different directories is evaluated independently.

To disable:

#+begin_src yaml
send_working_directory: false
#+end_src

** Redaction
  :PROPERTIES:
  :CUSTOM_ID: redaction
  :END:

guard-sh can redact sensitive values from commands before sending them to any LLM provider. Two methods are supported: pattern-based and Shannon entropy-based. Matches are replaced with =[REDACTED]=.

Redaction is configured globally under =redaction:= in your config, and can be overridden per-provider under =providers.NAME.redaction=.

#+begin_src yaml
redaction:
  pattern_based:
    enabled: true
    patterns:
      - '(?i)(password|passwd|secret|api[_-]?key|token|auth)\s*[=:]\s*\S+'
      - '(?i)--password\s+\S+'
      - 'AKIA[0-9A-Z]{16}'
  shannon_entropy_based:
    enabled: false
    threshold: 4.5   # bits/char — typical English ~3.5, secrets typically >4.5
    min_length: 20   # ignore tokens shorter than this
#+end_src

*** Pattern-based redaction

Uses Go regular expressions. Any token matching a pattern is replaced with =[REDACTED]=. On by default for all providers.

You can add your own patterns:

#+begin_src yaml
redaction:
  pattern_based:
    enabled: true
    patterns:
      - '(?i)(password|passwd|secret|api[_-]?key|token|auth)\s*[=:]\s*\S+'
      - '(?i)--password\s+\S+'
      - 'AKIA[0-9A-Z]{16}'
      - 'ghp_[a-zA-Z0-9]{36}'        # GitHub personal access tokens
      - 'xox[baprs]-[a-zA-Z0-9-]+'   # Slack tokens
#+end_src

Toggle per-provider:

#+begin_src bash
guard-sh redact list          # list all global redaction patterns
guard-sh redact pattern on    # interactively enable for a provider
guard-sh redact pattern off   # interactively disable for a provider
#+end_src

*** Shannon entropy-based redaction

Automatically detects high-entropy strings (API keys, random tokens, private key material) without requiring explicit patterns. Any whitespace-delimited token above the entropy threshold is replaced with =[REDACTED]=. Off by default.

Toggle per-provider:

#+begin_src bash
guard-sh redact entropy on    # interactively enable for a provider
guard-sh redact entropy off   # interactively disable for a provider
#+end_src

*** Per-provider overrides

A provider can override any part of the global redaction config:

#+begin_src yaml
providers:
  ollama:
    host: http://localhost:11434/api/chat
    redaction:                      # optional — overrides global for this provider
      pattern_based:
        enabled: false              # disable pattern redaction for local model
  gemini:
    api_key: YOUR_KEY
    redaction:
      shannon_entropy_based:
        enabled: true
        threshold: 4.0             # more sensitive than global default
        min_length: 16
#+end_src

Fields not specified in the per-provider block fall back to the global value.

** Provider-specific prompts

To use a different prompt for a specific provider, create =prompt_PROVIDERNAME.txt= in the same directory:

#+begin_src bash
~/.config/guard-sh/prompt.txt           # default, used by all providers
~/.config/guard-sh/prompt_ollama.txt    # used only when ollama is queried
~/.config/guard-sh/prompt_gemini.txt    # used only when gemini is queried
#+end_src

If a provider-specific file exists, it takes precedence over =prompt.txt= for that provider.

* Usage
  :PROPERTIES:
  :CUSTOM_ID: usage
  :END:

guard-sh is designed to run as a shell integration — it intercepts every command you type automatically. Once installed, you don't need to invoke it manually.

You can test it directly with:

#+begin_src bash
guard-sh check "rm -rf /"
#+end_src

This is useful for testing your configuration, but normal usage happens transparently through the shell hook.

** Help

#+begin_src bash
guard-sh help
#+end_src

Lists all commands and their usage.

** Session control

#+begin_src bash
guard-sh on      # enable for the current session
guard-sh off     # disable for the current session
#+end_src

** Global control

#+begin_src bash
guard-sh on --global    # auto-enable in every new terminal
guard-sh off --global   # don't auto-enable in new terminals
#+end_src

~off --global~ does not remove guard-sh from your shell — it only stops it from auto-enabling. You can still use ~guard-sh on~ in any terminal at any time.

** Healthcheck

#+begin_src bash
guard-sh healthcheck
#+end_src

Checks all configured providers without spending any tokens. Reports:

- API key and model validity for cloud providers; server connectivity for Ollama
- Response latency per provider
- Shell integration status (~.bashrc~ / ~.zshrc~ / ~config.fish~)
- Config errors (e.g. unknown provider names in ~provider_order~)

#+begin_example
  guard-sh healthcheck

  providers
  ollama      llama3.2                             ● ok  (12ms)
  gemini      gemini-3.1-flash-lite-preview        ● ok  (184ms)
  deepseek    deepseek-chat                        ✗ HTTP 401: Authentication Fails
  openai      gpt-4o-mini                          ● ok  (759ms)
  claude      claude-haiku-4-5-20251001            ● ok  (510ms)

  shell
  bash    ~/.bashrc                      ● present
  zsh     ~/.zshrc                       ○ not found
  fish    ~/.config/fish/config.fish     ○ not found
#+end_example

** Status

#+begin_src bash
guard-sh status
#+end_src

Shows session and global state, prompt path, timeout, cache stats, active providers, redaction patterns, shell integration status, and whitelist.

#+begin_example
  guard-sh

  session   ● on
  global    ● on
  config    ~/.config/guard-sh/config.yaml
  prompt    ~/.config/guard-sh/prompt.txt
  timeout   10s
  work dir  ● on
  cache     ● on
            1000 max entries
            42 entries, 8.3 KB

  providers
  1  gemini
     model                gemini-3.1-flash-lite-preview
     pattern redaction     ● on
     entropy redaction     ○ off

  2  ollama
     model                llama3.2
     pattern redaction     ○ off
     entropy redaction     ○ off

  3  deepseek
     model                deepseek-chat
     pattern redaction     ● on
     entropy redaction     ○ off

  redaction
  1  (?i)(password|passwd|secret|api[_-]?key|token|auth)\s*[=:]\s*\S+
  2  (?i)--password\s+\S+
  3  AKIA[0-9A-Z]{16}

  shell
  bash    ~/.bashrc                      ● present
  zsh     ~/.zshrc                       ○ not found
  fish    ~/.config/fish/config.fish     ○ not found

  whitelist
  1  ls
  2  cat
  3  echo
  4  pwd
  5  cd
  6  clear
  7  man
  8  help
  9  history
  10  which
  +3 more (to see all, run "guard-sh whitelist")
#+end_example

** Debug mode

#+begin_src bash
guard-sh check "<command>" --debug
#+end_src

Prints a step-by-step trace to stderr: whitelist result, per-provider cache check, redaction status, response time, and the final LLM response.

#+begin_example
  command   "rm -rf /"

  whitelist ○ miss

  providers
  openai      pattern ○ off  unchanged
              entropy ○ off  unchanged
                ✓ ok (1315ms)

  result    "This command will recursively delete everything from root."
#+end_example

With redaction enabled, the redacted form is shown before the query is sent:

#+begin_example
  providers
  openai      pattern ● on  → "rm -rf [REDACTED]"
              entropy ○ off  unchanged
                ✓ ok (891ms)
#+end_example

On a cache hit, the provider is not queried at all:

#+begin_example
  providers
  openai      cache ● hit  → "This command will recursively delete everything from root."
#+end_example

If a provider fails, guard-sh moves on to the next and shows why:

#+begin_example
  providers
  openai      pattern ○ off  unchanged
              entropy ○ off  unchanged
                ✗ HTTP 429: Rate limit exceeded (234ms), trying next
  gemini      pattern ○ off  unchanged
              entropy ○ off  unchanged
                ✓ ok (401ms)
#+end_example

** Setup

#+begin_src bash
guard-sh setup
#+end_src

Creates =~/.config/guard-sh/= with the default config and prompt, writes shell integration scripts (bash, zsh, fish), and adds them to your rc file based on your current shell. Existing files are not overwritten. This is the only step needed after downloading a pre-built binary.

** Version

#+begin_src bash
guard-sh version
#+end_src

Prints the current version (e.g. ~1.0.0~).

* Providers
  :PROPERTIES:
  :CUSTOM_ID: providers
  :END:

#+begin_src bash
guard-sh provider add      # interactively add a provider
guard-sh provider remove   # interactively remove a provider
guard-sh provider order    # interactively reorder providers
#+end_src

~provider add~ walks you through selecting a provider, picking a model, and entering an API key (or a full chat endpoint URL for Ollama, e.g. ~http://localhost:11434/api/chat~). The provider is saved to your config and added to ~provider_order~ automatically.

~provider remove~ lists your configured providers and removes the one you select.

~provider order~ opens an interactive list. Use arrow keys to navigate, =Enter= to grab a provider, arrow keys to move it, =Enter= again to place it. =s= saves and exits, =q= exits without saving.

* Whitelist
  :PROPERTIES:
  :CUSTOM_ID: whitelist
  :END:

Commands in the whitelist are always safe. The LLM is never called.

#+begin_src yaml
command_whitelist:
  - ls
  - git
#+end_src

- ~ls~, ~ls -la~, ~ls /tmp~ → all pass instantly
- ~ls && rm -rf /~ → ~rm~ is not whitelisted, so the full command goes to the LLM
- For chained commands, *all* parts must be whitelisted to skip the LLM

You can manage the whitelist from the command line:

#+begin_src bash
guard-sh whitelist              # list all whitelisted commands
guard-sh whitelist add git      # add a command
guard-sh whitelist remove git   # remove a command
#+end_src

* Cache
  :PROPERTIES:
  :CUSTOM_ID: cache
  :END:

guard-sh caches LLM responses locally so the same command is never sent to the API twice. The cache is stored at =~/.config/guard-sh/cache.json=.

When the cache is full, the least recently used entries are evicted first.

#+begin_src yaml
cache_enabled: true    # set to false to disable caching
cache_max_size: 1000   # max number of cached responses
#+end_src

You can also toggle caching from the command line:

#+begin_src bash
guard-sh cache on           # enable caching
guard-sh cache off          # disable caching
guard-sh cache size 500     # set max cache size
guard-sh cache clear        # delete all cached responses
#+end_src

* Shell support
  :PROPERTIES:
  :CUSTOM_ID: shell-support
  :END:

| Shell | Mechanism                          |
|-------+------------------------------------|
| bash  | ~shopt -s extdebug~ + ~DEBUG~ trap |
| zsh   | ZLE widget bound to Enter          |
| fish  | ~bind \n~/~\r~ mapped to custom function |

* License
  :PROPERTIES:
  :CUSTOM_ID: license
  :END:

GNU General Public License v3.0 — see [[file:LICENSE][LICENSE]].

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages