tightlip

skill
Security Audit
Warn
Health Warn
  • No license — Repository has no license file
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Warn
  • process.env — Environment variable access in plugins/tightlip/scripts/tightlip.py
  • process.env — Environment variable access in tests/test_tightlip.py
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Claude Code plugin that keeps secrets out of the model's context: redacts keys, tokens and passwords in tool output and blocks credential dumps.

README.md

tightlip

English · Tiếng Việt

A Claude Code plugin that keeps secrets out of the model's context. Named after tight-lipped: Claude keeps working on your project, but never sees your keys, tokens or passwords. This repo is both a marketplace (vntrungld) and the plugin itself (tightlip).

Hook What it does
SessionStart Names new sessions after the folder, e.g. my-backend 10-04 16:15, so Claude Code doesn't send your first prompt to a small model to generate a title. That request runs before any hook can block it.
UserPromptSubmit Blocks prompts that contain a secret. Right after the final gate trips, the next prompt is also blocked once, as a reminder.
PreToolUse Asks you to confirm before commands and file reads that expose secrets. Certain to: env, echo $DB_PASSWORD, gh auth token, kubectl get secret -o yaml, ~/.ssh/id_*, ~/.aws/credentials… May: .env files, shell history, *.tfvars/*.tfstate, docker inspect, docker compose config, terraform output, heroku config, helm get values, ps e… The dialog says why. If you allow it, the output is still redacted. With strict on, the first kind is denied outright; in bypassPermissions/dontAsk mode, where nobody is asked, it is denied too. Also denies any Write/Edit/Bash/MCP call that contains a [REDACTED:...] placeholder, so placeholders never overwrite real values.
PostToolUse Redacts secrets in every tool's output via updatedToolOutput, keeping the JSON shape intact.
PostToolBatch Final gate before each model request. If any result still holds a secret (usually output of a failed command, which hooks can't rewrite), it stops the turn and tells you where the secret is.
session.append (mod) On Claude Code versions with function hooks (mods), a second layer in hooks/register.ts redacts every row before the model reads it: failed command output, @ attachments and your own ! commands included. It hands the PostToolBatch gate the redacted results, so the turn goes on instead of stopping. When PreToolUse asks, the mod replaces Claude Code's dialog with a pane like Blast Radius's: what the call would expose, the names (never the values) of the secrets in the file or environment, and 1: Proceed / 2: Cancel (Cancel has the focus; no answer in 10 minutes cancels). It follows your language: Claude Code's language setting, else the language of your last prompt, else $LANG (Vietnamese or English). In a terminal narrower than about 144 columns it draws in the band above the prompt. Older versions ignore it and keep the command hooks alone.

The plugin only runs hooks and adds nothing to the model's context, so it costs no tokens. Requires Claude Code ≥ 2.1.121 and Python ≥ 3.8 (standard library only). The mod layer needs a Claude Code version with function hooks (tested on 2.1.296) and the plugin install; the standalone install has the command hooks only.

Install

1. In Claude Code:

/plugin marketplace add vntrungld/tightlip
/plugin install tightlip

Or from a shell:

claude plugin marketplace add vntrungld/tightlip
claude plugin install tightlip

If another marketplace also has a plugin named tightlip, use the full id tightlip@vntrungld.

When developing the plugin you can install straight from a clone: claude plugin marketplace add ./tightlip. Hooks then run directly from the repo folder (Claude Code still keeps a copy in ~/.claude/plugins/cache/), so edits to tightlip.py take effect on the next hook call, with no version bump or update. Changes to hooks.json need a new session.

2. Options (optional): run /plugin configure tightlip@vntrungld, or use /config.

Option Default Effect
name_sessions on Name sessions after the folder (see limitation 2)
quiet off Don't show a status line each time something is redacted
block_dotenv off Deny reading .env files instead of asking first (.env.example is still allowed)
strict off Deny commands that are certain to print secrets instead of asking first
allow_regex empty Values that fully match this regex are never redacted, e.g. test fixtures

3. Releasing (maintainers): bump version in plugins/tightlip/.claude-plugin/plugin.json and push. Users don't get a new release unless the version changes. They update with claude plugin update tightlip@vntrungld, or by enabling auto-update under /plugin → Marketplaces.

For a whole team

Add this to the project's .claude/settings.json:

{
  "extraKnownMarketplaces": {
    "vntrungld": { "source": { "source": "github", "repo": "vntrungld/tightlip" } }
  },
  "enabledPlugins": { "tightlip@vntrungld": true }
}

When someone trusts the project folder, the marketplace is registered automatically, but each person still runs /plugin install once. To enforce it across an organization, an admin sets both keys in managed settings (/etc/claude-code/managed-settings.json on Linux).

What the plugin can't do for you

Plugins aren't allowed to add permissions. Files you attach yourself with @ don't go through hooks, so to cover that case add this to ~/.claude/settings.json:

{
  "permissions": {
    "deny": [
      "Read(~/.ssh/id_*)",
      "Read(~/.aws/credentials)",
      "Read(~/.kube/config)",
      "Read(~/.docker/config.json)",
      "Read(~/.claude/.credentials.json)"
    ]
  }
}

Install without the plugin system

python3 standalone/install.py --dry-run   # preview settings.json after the merge
python3 standalone/install.py

The script copies the hook to ~/.claude/hooks/ and merges its config (including the deny rules above) into ~/.claude/settings.json, keeping your existing hooks and writing a backup first. In this mode options are environment variables: TIGHTLIP_QUIET=1, TIGHTLIP_BLOCK_DOTENV=1, TIGHTLIP_STRICT=1, TIGHTLIP_NAME_SESSIONS=0, TIGHTLIP_ALLOW_REGEX=..., TIGHTLIP_DISABLE=1. Use one install method, not both.

Testing

python3 -m unittest discover -v tests                                       # 45 tests
claude plugin test plugins/tightlip                                         # mod layer, 12 tests
echo 'DB_PASSWORD=abc123xyz' | python3 plugins/tightlip/scripts/tightlip.py --filter

Tested through the marketplace on Claude Code 2.1.289:

  • claude plugin details lists all 5 hooks.
  • In a real session, cat .env shows the model only placeholders.
  • With cat .env && exit 3, the turn stops before the output reaches the model.
  • No session-title request is sent.

What it detects

  • Tokens with a known format (about 50 rules): AWS, GitHub, GitLab, Slack, Stripe, Google, OpenAI/Anthropic/DeepSeek, DigitalOcean, Shopify, Atlassian, Sentry, PostHog phx_, npm, Docker Hub, Telegram, Grafana, Vault, Doppler, JWT, PEM private keys, Laravel APP_KEY…
  • By context:
    • dotenv/shell KEY=value, including lines prefixed by grep -n, grep -rn or cat -n.
    • YAML, JSON and PHP arrays with sensitive keys.
    • user:pass@ in URLs, Authorization/X-API-Key headers, ?token= query strings, --password= flags.
    • k8s env blocks, php artisan config:show output.
    • Secrets hard-coded in source: const apiKey: string = "…" (TS, Rust, Swift, Kotlin, C#, Go, Java, PHP, Ruby, Python), define('API_KEY', …), #define API_KEY "…", Elixir @api_key "…". In code the value must look random, so ordinary constants are left alone.
  • Long random values in UPPER_CASE=....

Run over laravel/framework, express, the Python standard library and npm packages (about 6,500 files), it only redacted test values that look like real secrets. Validation rules, env('APP_KEY'), translation files, lock-file hashes and constants in source code are left alone.

Limitations

  1. Output of failed commands. With the mod layer it is redacted like any other output. Without it (older Claude Code, standalone install), command hooks can't rewrite it, only stop the turn before it's sent. The content stays in the conversation, and if you just keep chatting the model will see it, so use /rewind (Esc Esc) to go back before that turn. The block message says where the secret is (command, or file and line such as config/.env.prod:12) so you can review it, but never the value itself.
  2. Session titles. With name_sessions on, sessions are named after the folder and time instead of a model-written summary. With it off, each session's first prompt is sent before hooks can block it.
  3. @ attachments and your own ! commands bypass command hooks. The mod layer redacts both. Without it, the output of ! cat .env goes straight into the conversation unredacted; for @ files, see the deny rules above. Either way the transcript file on disk keeps the original in a tool result's structured record (toolUseResult) and an attachment's payload, which the model doesn't read.
  4. Regexes have limits. These get through: values printed without their variable name (e.g. cut -d= -f2 .env), base64-encoded secrets, tokens with unusual formats. Add your own formats to FORMAT_RULES in scripts/tightlip.py.
  5. Hooks only change what the model sees. Commands still run for real, and OpenTelemetry (if enabled) still records the original output.
  6. The model could edit the plugin files via Bash. For a hard guarantee, an admin force-enables the plugin in managed settings and turns on allowManagedHooksOnly.
  7. Claude Code only. Codex doesn't let hooks replace tool output yet (openai/codex#38135).

Reviews (0)

No results found