agentfluent 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentfluent-0.4.0/.claude/hooks/block_secret_reads.py +156 -0
- agentfluent-0.4.0/.claude/hooks/detect_secrets_in_output.py +94 -0
- agentfluent-0.4.0/.claude/settings.json +36 -0
- agentfluent-0.4.0/.claude/specs/backlog-mvp.md +1467 -0
- agentfluent-0.4.0/.claude/specs/backlog-v0.3.md +655 -0
- agentfluent-0.4.0/.claude/specs/decisions.md +244 -0
- agentfluent-0.4.0/.claude/specs/plan-100-mcp-assessment.md +630 -0
- agentfluent-0.4.0/.claude/specs/plan-116-mcp-extraction.md +322 -0
- agentfluent-0.4.0/.claude/specs/prd-glossary.md +136 -0
- agentfluent-0.4.0/.claude/specs/prd-mvp.md +326 -0
- agentfluent-0.4.0/.claude/specs/prd-v0.3.md +281 -0
- agentfluent-0.4.0/.claude/specs/research-update-2026-04-15.md +127 -0
- agentfluent-0.4.0/.claude/specs/v0.4-scope-review.md +150 -0
- agentfluent-0.4.0/.github/ISSUE_TEMPLATE/bug_report.yml +81 -0
- agentfluent-0.4.0/.github/ISSUE_TEMPLATE/feature_request.yml +41 -0
- agentfluent-0.4.0/.github/PULL_REQUEST_TEMPLATE.md +21 -0
- agentfluent-0.4.0/.github/dependabot.yml +35 -0
- agentfluent-0.4.0/.github/workflows/ci.yml +37 -0
- agentfluent-0.4.0/.github/workflows/claude-review.yml +65 -0
- agentfluent-0.4.0/.github/workflows/dependabot-auto-merge.yml +34 -0
- agentfluent-0.4.0/.github/workflows/release-please.yml +76 -0
- agentfluent-0.4.0/.github/workflows/security-review.yml +68 -0
- agentfluent-0.4.0/.gitignore +40 -0
- agentfluent-0.4.0/.python-version +1 -0
- agentfluent-0.4.0/.release-please-manifest.json +3 -0
- agentfluent-0.4.0/CHANGELOG.md +108 -0
- agentfluent-0.4.0/CLAUDE.md +319 -0
- agentfluent-0.4.0/CONTRIBUTING.md +105 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/PKG-INFO +134 -76
- {agentfluent-0.2.0 → agentfluent-0.4.0}/README.md +122 -65
- agentfluent-0.4.0/SECURITY.md +24 -0
- agentfluent-0.4.0/docs/AGENT_ANALYTICS_RESEARCH.md +475 -0
- agentfluent-0.4.0/docs/GLOSSARY.md +820 -0
- agentfluent-0.4.0/docs/SECURITY.md +151 -0
- agentfluent-0.4.0/docs/codefluent_cli_review_042526.md +215 -0
- agentfluent-0.4.0/images/archive/v0.3/demo-analyze.svg +346 -0
- agentfluent-0.4.0/images/archive/v0.3/demo-config-check.svg +119 -0
- agentfluent-0.4.0/images/archive/v0.3/demo-diagnostics.svg +327 -0
- agentfluent-0.4.0/images/archive/v0.3/demo-subagents.svg +187 -0
- agentfluent-0.4.0/images/demo-analyze.svg +346 -0
- agentfluent-0.4.0/images/demo-config-check.svg +119 -0
- agentfluent-0.4.0/images/demo-diagnostics.svg +327 -0
- agentfluent-0.4.0/images/demo-subagents.svg +187 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/pyproject.toml +21 -3
- agentfluent-0.4.0/release-please-config.json +15 -0
- agentfluent-0.4.0/scripts/calibration/README.md +84 -0
- agentfluent-0.4.0/scripts/calibration/build_notebook.py +658 -0
- agentfluent-0.4.0/scripts/calibration/threshold_validation.ipynb +1396 -0
- agentfluent-0.4.0/scripts/generate_glossary_md.py +28 -0
- agentfluent-0.4.0/scripts/generate_readme_screenshots.py +182 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/agents/extractor.py +16 -12
- agentfluent-0.4.0/src/agentfluent/agents/models.py +107 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/agent_metrics.py +31 -6
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/pipeline.py +72 -2
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/pricing.py +4 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/commands/analyze.py +82 -11
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/commands/config_check.py +17 -2
- agentfluent-0.4.0/src/agentfluent/cli/commands/explain.py +188 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/commands/list_cmd.py +38 -17
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/formatters/helpers.py +22 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/formatters/table.py +180 -39
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/main.py +42 -1
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/__init__.py +6 -1
- agentfluent-0.4.0/src/agentfluent/config/mcp_discovery.py +334 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/models.py +43 -1
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/scanner.py +2 -1
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/core/discovery.py +8 -2
- agentfluent-0.4.0/src/agentfluent/core/parser.py +375 -0
- agentfluent-0.4.0/src/agentfluent/core/paths.py +90 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/core/session.py +29 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/__init__.py +7 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/aggregation.py +159 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/builtin_actions.py +77 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/correlator.py +650 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/delegation.py +528 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/mcp_assessment.py +346 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/model_routing.py +337 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/models.py +284 -0
- agentfluent-0.4.0/src/agentfluent/diagnostics/pipeline.py +275 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/diagnostics/signals.py +8 -3
- agentfluent-0.4.0/src/agentfluent/diagnostics/trace_signals.py +361 -0
- agentfluent-0.4.0/src/agentfluent/glossary/__init__.py +26 -0
- agentfluent-0.4.0/src/agentfluent/glossary/loader.py +157 -0
- agentfluent-0.4.0/src/agentfluent/glossary/models.py +85 -0
- agentfluent-0.4.0/src/agentfluent/glossary/render.py +136 -0
- agentfluent-0.4.0/src/agentfluent/glossary/terms.yaml +653 -0
- agentfluent-0.4.0/src/agentfluent/py.typed +0 -0
- agentfluent-0.4.0/src/agentfluent/traces/__init__.py +0 -0
- agentfluent-0.4.0/src/agentfluent/traces/discovery.py +75 -0
- agentfluent-0.4.0/src/agentfluent/traces/linker.py +50 -0
- agentfluent-0.4.0/src/agentfluent/traces/models.py +119 -0
- agentfluent-0.4.0/src/agentfluent/traces/parser.py +207 -0
- agentfluent-0.4.0/src/agentfluent/traces/retry.py +91 -0
- agentfluent-0.4.0/tests/__init__.py +0 -0
- agentfluent-0.4.0/tests/_builders.py +208 -0
- agentfluent-0.4.0/tests/conftest.py +141 -0
- agentfluent-0.4.0/tests/fixtures/.gitkeep +0 -0
- agentfluent-0.4.0/tests/fixtures/agents/empty_prompt.md +12 -0
- agentfluent-0.4.0/tests/fixtures/agents/no_frontmatter.md +4 -0
- agentfluent-0.4.0/tests/fixtures/agents/no_tools.md +8 -0
- agentfluent-0.4.0/tests/fixtures/agents/vague_description.md +9 -0
- agentfluent-0.4.0/tests/fixtures/agents/well_configured.md +41 -0
- agentfluent-0.4.0/tests/fixtures/mcp/claude_user_only.json +15 -0
- agentfluent-0.4.0/tests/fixtures/mcp/claude_user_with_disabled.json +14 -0
- agentfluent-0.4.0/tests/fixtures/session_basic.jsonl +4 -0
- agentfluent-0.4.0/tests/fixtures/session_block_per_line.jsonl +7 -0
- agentfluent-0.4.0/tests/fixtures/session_malformed.jsonl +3 -0
- agentfluent-0.4.0/tests/fixtures/session_skip_types.jsonl +8 -0
- agentfluent-0.4.0/tests/fixtures/session_streaming_dupes.jsonl +8 -0
- agentfluent-0.4.0/tests/fixtures/session_with_agent.jsonl +5 -0
- agentfluent-0.4.0/tests/fixtures/session_with_tool_calls.jsonl +5 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-basic.jsonl +8 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-empty.jsonl +0 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-errors.jsonl +5 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-large.jsonl +46 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-malformed.jsonl +7 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-retry.jsonl +7 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-streaming-dupes.jsonl +6 -0
- agentfluent-0.4.0/tests/fixtures/subagents/agent-stuck.jsonl +11 -0
- agentfluent-0.4.0/tests/integration/__init__.py +0 -0
- agentfluent-0.4.0/tests/integration/test_agent_extraction.py +63 -0
- agentfluent-0.4.0/tests/integration/test_analytics.py +94 -0
- agentfluent-0.4.0/tests/integration/test_config_assessment.py +54 -0
- agentfluent-0.4.0/tests/integration/test_diagnostics.py +69 -0
- agentfluent-0.4.0/tests/integration/test_real_sessions.py +135 -0
- agentfluent-0.4.0/tests/integration/test_subagent_traces.py +180 -0
- agentfluent-0.4.0/tests/unit/__init__.py +0 -0
- agentfluent-0.4.0/tests/unit/cli/__init__.py +0 -0
- agentfluent-0.4.0/tests/unit/cli/conftest.py +163 -0
- agentfluent-0.4.0/tests/unit/cli/test_claude_config_dir.py +196 -0
- agentfluent-0.4.0/tests/unit/cli/test_cost_labeling.py +73 -0
- agentfluent-0.4.0/tests/unit/cli/test_deep_diagnostics_formatting.py +259 -0
- agentfluent-0.4.0/tests/unit/cli/test_diagnostics_default.py +126 -0
- agentfluent-0.4.0/tests/unit/cli/test_diagnostics_smoke.py +110 -0
- agentfluent-0.4.0/tests/unit/cli/test_exit_codes.py +108 -0
- agentfluent-0.4.0/tests/unit/cli/test_explain.py +98 -0
- agentfluent-0.4.0/tests/unit/cli/test_glossary_footer.py +127 -0
- agentfluent-0.4.0/tests/unit/cli/test_help.py +46 -0
- agentfluent-0.4.0/tests/unit/cli/test_json_alias.py +112 -0
- agentfluent-0.4.0/tests/unit/cli/test_json_output.py +104 -0
- agentfluent-0.4.0/tests/unit/cli/test_output_modes.py +73 -0
- agentfluent-0.4.0/tests/unit/cli/test_parse_warnings.py +39 -0
- agentfluent-0.4.0/tests/unit/test_agent_metrics.py +239 -0
- agentfluent-0.4.0/tests/unit/test_agent_models.py +88 -0
- agentfluent-0.4.0/tests/unit/test_correlator.py +612 -0
- agentfluent-0.4.0/tests/unit/test_dedup.py +262 -0
- agentfluent-0.4.0/tests/unit/test_delegation.py +492 -0
- agentfluent-0.4.0/tests/unit/test_delegation_yaml_draft.py +91 -0
- agentfluent-0.4.0/tests/unit/test_diagnostics.py +93 -0
- agentfluent-0.4.0/tests/unit/test_diagnostics_pipeline.py +515 -0
- agentfluent-0.4.0/tests/unit/test_discovery.py +169 -0
- agentfluent-0.4.0/tests/unit/test_extractor.py +299 -0
- agentfluent-0.4.0/tests/unit/test_glossary_drift.py +34 -0
- agentfluent-0.4.0/tests/unit/test_glossary_loader.py +230 -0
- agentfluent-0.4.0/tests/unit/test_glossary_render.py +103 -0
- agentfluent-0.4.0/tests/unit/test_mcp_assessment.py +455 -0
- agentfluent-0.4.0/tests/unit/test_mcp_discovery.py +554 -0
- agentfluent-0.4.0/tests/unit/test_model_routing.py +396 -0
- agentfluent-0.4.0/tests/unit/test_parser.py +497 -0
- agentfluent-0.4.0/tests/unit/test_paths.py +41 -0
- agentfluent-0.4.0/tests/unit/test_pipeline.py +72 -0
- agentfluent-0.4.0/tests/unit/test_pricing.py +141 -0
- agentfluent-0.4.0/tests/unit/test_recommendation_aggregation.py +310 -0
- agentfluent-0.4.0/tests/unit/test_scanner.py +107 -0
- agentfluent-0.4.0/tests/unit/test_scoring.py +212 -0
- agentfluent-0.4.0/tests/unit/test_session_models.py +191 -0
- agentfluent-0.4.0/tests/unit/test_signals.py +198 -0
- agentfluent-0.4.0/tests/unit/test_smoke.py +44 -0
- agentfluent-0.4.0/tests/unit/test_tokens.py +242 -0
- agentfluent-0.4.0/tests/unit/test_tools.py +102 -0
- agentfluent-0.4.0/tests/unit/test_trace_signals.py +309 -0
- agentfluent-0.4.0/tests/unit/test_traces_discovery.py +143 -0
- agentfluent-0.4.0/tests/unit/test_traces_fixtures.py +204 -0
- agentfluent-0.4.0/tests/unit/test_traces_linker.py +298 -0
- agentfluent-0.4.0/tests/unit/test_traces_models.py +318 -0
- agentfluent-0.4.0/tests/unit/test_traces_parser.py +460 -0
- agentfluent-0.4.0/tests/unit/test_traces_retry.py +310 -0
- agentfluent-0.4.0/uv.lock +2660 -0
- agentfluent-0.2.0/src/agentfluent/agents/models.py +0 -71
- agentfluent-0.2.0/src/agentfluent/core/parser.py +0 -255
- agentfluent-0.2.0/src/agentfluent/diagnostics/__init__.py +0 -51
- agentfluent-0.2.0/src/agentfluent/diagnostics/correlator.py +0 -248
- agentfluent-0.2.0/src/agentfluent/diagnostics/models.py +0 -75
- /agentfluent-0.2.0/src/agentfluent/agents/__init__.py → /agentfluent-0.4.0/.claude/specs/.gitkeep +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/LICENSE +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/__init__.py +0 -0
- {agentfluent-0.2.0/src/agentfluent/analytics → agentfluent-0.4.0/src/agentfluent/agents}/__init__.py +0 -0
- {agentfluent-0.2.0/src/agentfluent/cli → agentfluent-0.4.0/src/agentfluent/analytics}/__init__.py +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/tokens.py +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/tools.py +0 -0
- {agentfluent-0.2.0/src/agentfluent/cli/commands → agentfluent-0.4.0/src/agentfluent/cli}/__init__.py +0 -0
- {agentfluent-0.2.0/src/agentfluent/cli/formatters → agentfluent-0.4.0/src/agentfluent/cli/commands}/__init__.py +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/exit_codes.py +0 -0
- {agentfluent-0.2.0/src/agentfluent/core → agentfluent-0.4.0/src/agentfluent/cli/formatters}/__init__.py +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/formatters/json_output.py +0 -0
- {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/scoring.py +0 -0
- /agentfluent-0.2.0/src/agentfluent/py.typed → /agentfluent-0.4.0/src/agentfluent/core/__init__.py +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse hook: block reads of files likely to contain credentials.
|
|
3
|
+
|
|
4
|
+
Receives the PreToolUse event JSON on stdin. Denies the tool call when the
|
|
5
|
+
target file path (or Bash command argument) matches known credential files:
|
|
6
|
+
.env variants, shell rc files, SSH private keys, and named secrets files.
|
|
7
|
+
|
|
8
|
+
Emits a JSON decision on stdout and exits 0 (the modern pattern); exit 2
|
|
9
|
+
+ stderr is the legacy fallback but not used here.
|
|
10
|
+
|
|
11
|
+
Cross-platform: stdlib only, no shell dependencies.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import PurePath
|
|
20
|
+
|
|
21
|
+
# The BLOCKED_BASENAMES set and the CREDENTIAL_TOKEN_PATTERNS list must stay in
|
|
22
|
+
# sync — they express the same credential-file list in two matching contexts
|
|
23
|
+
# (exact basename vs. substring-in-command-or-pattern). Update both together.
|
|
24
|
+
BLOCKED_BASENAMES: frozenset[str] = frozenset(
|
|
25
|
+
{
|
|
26
|
+
".env",
|
|
27
|
+
".envrc",
|
|
28
|
+
"credentials",
|
|
29
|
+
"credentials.json",
|
|
30
|
+
"secrets.yaml",
|
|
31
|
+
"secrets.yml",
|
|
32
|
+
"secrets.json",
|
|
33
|
+
".bashrc",
|
|
34
|
+
".bash_profile",
|
|
35
|
+
".profile",
|
|
36
|
+
".zshrc",
|
|
37
|
+
".zshenv",
|
|
38
|
+
".zprofile",
|
|
39
|
+
"id_rsa",
|
|
40
|
+
"id_ed25519",
|
|
41
|
+
"id_ecdsa",
|
|
42
|
+
"id_dsa",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
BLOCKED_SUFFIXES: frozenset[str] = frozenset({".pem"})
|
|
47
|
+
|
|
48
|
+
CREDENTIAL_TOKEN_PATTERNS: list[str] = [
|
|
49
|
+
r"\.env(\b|[._-][A-Za-z0-9_-]+)",
|
|
50
|
+
r"\.envrc\b",
|
|
51
|
+
r"credentials\.json\b",
|
|
52
|
+
r"secrets\.ya?ml\b",
|
|
53
|
+
r"secrets\.json\b",
|
|
54
|
+
r"\.bashrc\b",
|
|
55
|
+
r"\.bash_profile\b",
|
|
56
|
+
r"\.profile\b",
|
|
57
|
+
r"\.zshrc\b",
|
|
58
|
+
r"\.zshenv\b",
|
|
59
|
+
r"\.zprofile\b",
|
|
60
|
+
r"\bid_rsa\b",
|
|
61
|
+
r"\bid_ed25519\b",
|
|
62
|
+
r"\bid_ecdsa\b",
|
|
63
|
+
r"\bid_dsa\b",
|
|
64
|
+
r"\.pem\b",
|
|
65
|
+
]
|
|
66
|
+
CREDENTIAL_TOKEN_REGEX = re.compile("|".join(CREDENTIAL_TOKEN_PATTERNS), re.IGNORECASE)
|
|
67
|
+
|
|
68
|
+
FILE_PATH_TOOLS = {"Read", "Edit", "Write", "NotebookEdit"}
|
|
69
|
+
PATH_SEARCH_TOOLS = {"Grep", "Glob"}
|
|
70
|
+
|
|
71
|
+
DENY_REASON = (
|
|
72
|
+
"Blocked by AgentFluent secrets-protection hook (.claude/hooks/block_secret_reads.py). "
|
|
73
|
+
"This file is a likely credential source (.env, shell rc, SSH key, or named secrets file). "
|
|
74
|
+
"Reading it would persist its contents in the Claude Code session JSONL. "
|
|
75
|
+
"If you need to verify the file exists, use `test -f <path>`. "
|
|
76
|
+
"See docs/SECURITY.md for the full policy."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def path_is_blocked(path_str: str) -> bool:
|
|
81
|
+
if not path_str:
|
|
82
|
+
return False
|
|
83
|
+
p = PurePath(path_str)
|
|
84
|
+
name = p.name
|
|
85
|
+
if name in BLOCKED_BASENAMES:
|
|
86
|
+
return True
|
|
87
|
+
if p.suffix in BLOCKED_SUFFIXES:
|
|
88
|
+
return True
|
|
89
|
+
if name.startswith((".env.", ".env-", ".env_")):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def bash_command_is_blocked(command: str) -> bool:
|
|
95
|
+
if not command:
|
|
96
|
+
return False
|
|
97
|
+
return CREDENTIAL_TOKEN_REGEX.search(command) is not None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check(event: dict) -> tuple[bool, str]:
|
|
101
|
+
tool_name = event.get("tool_name", "")
|
|
102
|
+
tool_input = event.get("tool_input") or {}
|
|
103
|
+
|
|
104
|
+
if tool_name in FILE_PATH_TOOLS:
|
|
105
|
+
path = tool_input.get("file_path") or tool_input.get("notebook_path") or ""
|
|
106
|
+
if path_is_blocked(path):
|
|
107
|
+
return True, f"{DENY_REASON} (path: {path})"
|
|
108
|
+
|
|
109
|
+
if tool_name in PATH_SEARCH_TOOLS:
|
|
110
|
+
path = tool_input.get("path") or ""
|
|
111
|
+
pattern = tool_input.get("pattern") or ""
|
|
112
|
+
if path and path_is_blocked(path):
|
|
113
|
+
return True, f"{DENY_REASON} (search path: {path})"
|
|
114
|
+
if pattern and CREDENTIAL_TOKEN_REGEX.search(pattern):
|
|
115
|
+
return True, f"{DENY_REASON} (search pattern targets credential file: {pattern})"
|
|
116
|
+
|
|
117
|
+
if tool_name == "Bash":
|
|
118
|
+
command = tool_input.get("command") or ""
|
|
119
|
+
if bash_command_is_blocked(command):
|
|
120
|
+
return True, f"{DENY_REASON} (command: {command[:200]})"
|
|
121
|
+
|
|
122
|
+
return False, ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def emit_decision(decision: str, reason: str) -> None:
|
|
126
|
+
payload = {
|
|
127
|
+
"hookSpecificOutput": {
|
|
128
|
+
"hookEventName": "PreToolUse",
|
|
129
|
+
"permissionDecision": decision,
|
|
130
|
+
"permissionDecisionReason": reason,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
sys.stdout.write(json.dumps(payload))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main() -> int:
|
|
137
|
+
try:
|
|
138
|
+
event = json.load(sys.stdin)
|
|
139
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
140
|
+
# Fail closed: this is a security-critical PreToolUse hook. If we can't
|
|
141
|
+
# parse the event we cannot confirm the call is safe, so deny rather
|
|
142
|
+
# than allow through a malformed event of unknown provenance.
|
|
143
|
+
print(
|
|
144
|
+
f"block_secret_reads: failed to parse hook event JSON, denying by default: {e}",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
return 2
|
|
148
|
+
|
|
149
|
+
blocked, reason = check(event)
|
|
150
|
+
if blocked:
|
|
151
|
+
emit_decision("deny", reason)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
sys.exit(main())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook: block Claude from reasoning about tool output with secrets.
|
|
3
|
+
|
|
4
|
+
PostToolUse cannot redact non-MCP tool output inline. Instead, this hook uses
|
|
5
|
+
the detect-and-block pattern: if the tool response contains a known API key
|
|
6
|
+
or token pattern, it emits `{"decision": "block", "reason": ...}` so Claude
|
|
7
|
+
Code surfaces a block signal alongside the result and Claude knows not to
|
|
8
|
+
echo, summarize, or otherwise act on the leaked value.
|
|
9
|
+
|
|
10
|
+
Caveat: PostToolUse fires AFTER the tool has executed. The raw output is
|
|
11
|
+
already persisted in the session JSONL, and Claude still technically receives
|
|
12
|
+
the tool_result in-session. This hook prevents further propagation (summaries,
|
|
13
|
+
follow-up prompts quoting the value) but does NOT prevent the on-disk leak.
|
|
14
|
+
The PreToolUse block_secret_reads.py hook is the primary defense against
|
|
15
|
+
on-disk leakage; this is a secondary guard.
|
|
16
|
+
|
|
17
|
+
Cross-platform: stdlib only.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
SECRET_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
27
|
+
(re.compile(r"sk-ant-[A-Za-z0-9_-]{20,}"), "anthropic-key"),
|
|
28
|
+
(re.compile(r"sk-proj-[A-Za-z0-9_-]{20,}"), "openai-project-key"),
|
|
29
|
+
(re.compile(r"sk-[A-Za-z0-9]{40,}"), "openai-key-legacy"),
|
|
30
|
+
(re.compile(r"ghp_[A-Za-z0-9]{30,}"), "github-pat-classic"),
|
|
31
|
+
(re.compile(r"github_pat_[A-Za-z0-9_]{40,}"), "github-pat-fine"),
|
|
32
|
+
(re.compile(r"AKIA[A-Z0-9]{16}"), "aws-access-key-id"),
|
|
33
|
+
(re.compile(r"AIza[A-Za-z0-9_-]{35}"), "gcp-api-key"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
DENY_REASON_TEMPLATE = (
|
|
37
|
+
"Blocked by AgentFluent secrets-protection hook "
|
|
38
|
+
"(.claude/hooks/detect_secrets_in_output.py). "
|
|
39
|
+
"Tool output contained a value matching a known credential pattern: {kinds}. "
|
|
40
|
+
"Claude is being prevented from reasoning about this output to avoid further "
|
|
41
|
+
"propagation (e.g. echoing in summaries). "
|
|
42
|
+
"IMPORTANT: the raw value has already been persisted in the session JSONL "
|
|
43
|
+
"because PostToolUse fires after tool execution. Rotate any key that may have "
|
|
44
|
+
"leaked and see docs/SECURITY.md for full remediation steps."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def stringify_response(resp: object) -> str:
|
|
49
|
+
"""Coerce tool_response (dict, list, str, etc.) into a single searchable string."""
|
|
50
|
+
if isinstance(resp, str):
|
|
51
|
+
return resp
|
|
52
|
+
try:
|
|
53
|
+
return json.dumps(resp, default=str)
|
|
54
|
+
except (TypeError, ValueError):
|
|
55
|
+
return str(resp)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_secret_kinds(text: str) -> list[str]:
|
|
59
|
+
kinds: list[str] = []
|
|
60
|
+
for pattern, label in SECRET_PATTERNS:
|
|
61
|
+
if pattern.search(text):
|
|
62
|
+
kinds.append(label)
|
|
63
|
+
return kinds
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def emit_block(reason: str) -> None:
|
|
67
|
+
payload = {"decision": "block", "reason": reason}
|
|
68
|
+
sys.stdout.write(json.dumps(payload))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> int:
|
|
72
|
+
try:
|
|
73
|
+
event = json.load(sys.stdin)
|
|
74
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
75
|
+
print(
|
|
76
|
+
f"detect_secrets_in_output: failed to parse hook event JSON: {e}",
|
|
77
|
+
file=sys.stderr,
|
|
78
|
+
)
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
response = event.get("tool_response")
|
|
82
|
+
if response is None:
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
text = stringify_response(response)
|
|
86
|
+
kinds = find_secret_kinds(text)
|
|
87
|
+
if kinds:
|
|
88
|
+
reason = DENY_REASON_TEMPLATE.format(kinds=", ".join(sorted(set(kinds))))
|
|
89
|
+
emit_block(reason)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
sys.exit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Read|Edit|Write|Grep|Glob|NotebookEdit|Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "python3 .claude/hooks/block_secret_reads.py"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Read|Grep|Bash",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "python3 .claude/hooks/detect_secrets_in_output.py"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"Stop": [
|
|
26
|
+
{
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "if git diff --name-only HEAD 2>/dev/null | grep -qE '\\.(ts|js|py|tsx|jsx)$'; then echo 'Reminder: Run /simplify to review changed code for reuse, quality, and efficiency before committing.'; fi"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|