feat(sponge): add three-layer privacy filtering for shell history

conf.d/sponge_privacy.fish registers patterns and filters that prevent
credentials from reaching fish history:

  Layer 1 (static regex, universal): auth flags, env assignments,
  credential-bearing URLs, Authorization headers, sshpass, docker
  login, openssl -passin/-passout

  Layer 2 (dynamic values, session globals): on the first prompt, after
  secrets.fish has loaded, reads the literal values of all exported
  credential-named vars (TOKEN, PASSWORD, SECRET, etc.), escapes them
  for regex, and merges them with the static patterns as a session
  global — auto-refreshes on login so rotated tokens are never stale

  Layer 3 (per-command filter, sponge_filter_secrets): catches
  credentials in variables exported mid-session (e.g. project .env)

Also exempts functions/sponge_filter_secrets.fish from the sponge_*
gitignore glob so our custom filter is committed alongside the config.
This commit is contained in:
2026-06-08 22:23:29 -04:00
parent d21aa4ed4b
commit bceff8f4ec
5 changed files with 200 additions and 0 deletions
+1
View File
@@ -70,6 +70,7 @@ completions/fisher.fish
conf.d/sponge.fish
functions/_sponge*.fish
functions/sponge_*.fish
!functions/sponge_filter_secrets.fish
# ─────────────────── AI Sessions and Rules ──────────────────
# Matches CLAUDE.md, .claud*, etc.
+113
View File
@@ -0,0 +1,113 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# ╭──────────────────────────────────────────────────────────╮
# │ Sponge Privacy Pattern Registration │
# ╰──────────────────────────────────────────────────────────╯
#
# Two-layer approach to keeping credentials out of shell history:
#
# Layer 1 — Static patterns (registered as universal, persistent):
# Covers structural signatures: auth flags, env var assignments,
# credential-bearing URLs, Authorization headers, sshpass, etc.
# Patterns are added idempotently; user additions are preserved.
#
# Layer 2 — Dynamic secret values (registered as session globals):
# On the first prompt (after secrets.fish has loaded), reads the
# literal values of all exported variables whose names indicate
# credentials (TOKEN, PASSWORD, SECRET, KEY, etc.) and adds them
# as a session-scoped pattern overlay. Refreshes automatically
# each login, so rotated tokens never leave stale patterns behind.
#
# Layer 3 — Per-command filter (sponge_filter_secrets):
# Catches mid-session variables set after login — e.g. a token
# exported interactively or sourced from a project .env file.
#
# To add your own persistent patterns:
# set -U -a sponge_regex_patterns 'your-regex-here'
if not status is-interactive
return
end
# Only register if sponge is loaded
if not set -q sponge_version
return
end
# ──────────────────── Layer 1: Static patterns ────────────────────
set -l _privacy_patterns
# Common auth flags with values: --password x, --token x, --passphrase x, --api-key x
set -a _privacy_patterns '--(?:password|passwd|passphrase|token|secret|api[-_]key)(?:\s+|=)\S+'
# Inline env var assignments with sensitive names: GITHUB_TOKEN=xxx, MY_API_KEY=abc
set -a _privacy_patterns '(?i)\b[A-Z][A-Z0-9_]*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|PRIVATE_KEY|ACCESS_KEY|AUTH_KEY|CREDENTIAL)[A-Z0-9_]*=\S+'
# Fish set with sensitive variable names: set -gx GITHUB_TOKEN xxx, set -U MY_SECRET yyy
set -a _privacy_patterns '(?i)set\s+-\S+\s+\S*(?:password|passwd|token|secret|api.?key|private.?key|access.?key|credential)\S*\s+\S+'
# URLs with embedded credentials: https://user:password@host
set -a _privacy_patterns 'https?://[^:@\s]+:[^@\s]+@'
# HTTP Authorization headers: curl -H "Authorization: Bearer xxx"
set -a _privacy_patterns 'curl\s.*[Aa]uthorization:'
# Basic auth flags: curl -u user:pass, wget --user user --password pass
set -a _privacy_patterns '(?:curl|wget)\s.*(?:-u|--user)\s+\S+:\S+'
# sshpass — exposes credentials as a CLI argument by design
set -a _privacy_patterns '\bsshpass\b'
# Docker login with inline password
set -a _privacy_patterns 'docker\s+login\s.*(?:-p|--password)\s+\S+'
# openssl passphrase arguments: -passin pass:xxx, -passout env:VAR
set -a _privacy_patterns 'openssl\s.*-pass(?:in|out)\s+\S+'
# Idempotent registration into universal sponge_regex_patterns
for _pattern in $_privacy_patterns
if not contains -- $_pattern $sponge_regex_patterns
set -U -a sponge_regex_patterns $_pattern
end
end
# ──────────── Layer 2: Dynamic secret values (session globals) ────────────
# Runs once on the first prompt — by which point config.fish and secrets.fish
# have fully loaded, so all secret env vars are in scope.
# Builds a session-scoped global that combines the universal static patterns
# with the literal values of any credential-holding env vars. Globals shadow
# universals in Fish, so the combined list is what sponge sees for this session.
function __sponge_register_secret_values --on-event fish_prompt
functions --erase __sponge_register_secret_values # run exactly once
set -l secret_values
set -l sensitive_vars (set --names --export | string match --regex -- \
'(?i)(?:TOKEN|PASSWORD|PASSWD|SECRET|API[_-]KEY|PRIVATE[_-]KEY|ACCESS[_-]KEY|AUTH[_-]KEY|CREDENTIAL|KOPIA_PASSWORD)')
for var in $sensitive_vars
set -l value $$var
# Skip empty, short, or path-like values
test (string length -- $value) -gt 8; or continue
string match --quiet --regex '^[/~]' -- $value; and continue
set -a secret_values (string escape --style=regex -- $value)
end
if test (count $secret_values) -gt 0
# Merge static universals + dynamic values into a session global
set -g sponge_regex_patterns $sponge_regex_patterns $secret_values
end
end
# ──────────── Layer 3: Mid-session filter (sponge_filter_secrets) ─────────
# Catches credentials in variables exported after login (e.g. project .env files).
# Only register if not already in sponge_filters.
if functions --query sponge_filter_secrets
if not contains -- sponge_filter_secrets $sponge_filters
set -U -a sponge_filters sponge_filter_secrets
end
end
+3
View File
@@ -168,6 +168,9 @@ plugins=# 8. FISHER PLUGINS
fisher=# 8. FISHER PLUGINS
fisher-managed=## Fisher-Managed Plugins
sponge=## Fisher-Managed Plugins
sponge-filtering=## Sponge History Filtering
history-filtering=## Sponge History Filtering
privacy=## Sponge History Filtering
bundled-plugins=## Bundled Plugin Functionality
fish-plugins-manifest=## fish_plugins Manifest
+32
View File
@@ -1426,6 +1426,38 @@ commit them. Fisher installs and updates them automatically.
jorgebucaran/fisher Plugin manager itself
meaningful-ooo/sponge Remove failed commands from history
## Sponge History Filtering
Sponge removes failed commands from history and, via conf.d/sponge_privacy.fish,
also filters privacy-sensitive commands through three layers:
Layer 1 — Static patterns (universal, persistent across sessions):
Commands matching any of these structural signatures are never recorded:
--password / --token / --passphrase / --api-key flags with values
Inline env assignments: GITHUB_TOKEN=xxx, MY_API_KEY=abc
Fish set with sensitive names: set -gx GITHUB_TOKEN xxx
URLs with embedded credentials: https://user:pass@host
HTTP Authorization headers: curl -H "Authorization: ..."
Basic auth flags: curl -u user:pass
sshpass, docker login -p, openssl -passin/-passout
Layer 2 — Dynamic secret values (session globals, refreshed each login):
On the first prompt, after secrets.fish has loaded, the literal values of
all exported variables whose names suggest credentials (TOKEN, PASSWORD,
SECRET, API_KEY, etc.) are collected, regex-escaped, and added as a
session-scoped overlay. Because globals shadow universals in Fish, the
combined list is what sponge sees. Rotating a token takes effect on the
next login automatically.
Layer 3 — Per-command filter (sponge_filter_secrets):
Catches credentials in variables exported after login, such as tokens
sourced from a project .env file mid-session.
To add your own persistent patterns:
set -U -a sponge_regex_patterns 'your-regex-here'
## Bundled Plugin Functionality
The remaining plugin functionality is bundled directly with this config rather
+51
View File
@@ -0,0 +1,51 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# sponge_filter_secrets <command> <exit_code> <previously_in_history>
#
# DESCRIPTION
# Custom sponge filter that prevents commands from being stored in history
# when they contain the literal value of any exported environment variable
# whose name indicates it holds a credential (TOKEN, PASSWORD, SECRET,
# API_KEY, etc.). This catches shell-expansion leakage where a variable
# value is embedded directly in the command string at execution time — a
# case that static regex patterns cannot cover.
#
# Any variable whose name matches the sensitive-name heuristic and whose
# value is longer than 8 characters (excluding bare paths) is checked.
# The value is escaped for literal regex matching before comparison.
#
# ARGUMENTS
# command The exact command that was entered
# exit_code Exit code of the command (unused)
# previously_in_history "true"/"false" flag (unused)
#
# RETURNS
# 0 Command contains a secret value — filter out of history
# 1 No secret value found — keep in history
#
# EXAMPLE
# # Register with sponge (done automatically by conf.d/sponge_privacy.fish):
# set -U -a sponge_filters sponge_filter_secrets
function sponge_filter_secrets --argument-names command
# Find all exported variables with security-sensitive names
set -l sensitive_vars (set --names --export | string match --regex -- \
'(?i)(?:TOKEN|PASSWORD|PASSWD|SECRET|API[_-]KEY|PRIVATE[_-]KEY|ACCESS[_-]KEY|AUTH[_-]KEY|CREDENTIAL|KOPIA_PASSWORD)')
for var in $sensitive_vars
set -l value $$var
# Skip empty, short, or path-like values — not real credentials
test (string length -- $value) -gt 8; or continue
string match --quiet --regex '^[/~]' -- $value; and continue
# Filter if the literal value appears anywhere in the command
if string match --quiet --regex -- (string escape --style=regex -- $value) $command
return 0
end
end
return 1
end