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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user