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.
Files changed (197) hide show
  1. agentfluent-0.4.0/.claude/hooks/block_secret_reads.py +156 -0
  2. agentfluent-0.4.0/.claude/hooks/detect_secrets_in_output.py +94 -0
  3. agentfluent-0.4.0/.claude/settings.json +36 -0
  4. agentfluent-0.4.0/.claude/specs/backlog-mvp.md +1467 -0
  5. agentfluent-0.4.0/.claude/specs/backlog-v0.3.md +655 -0
  6. agentfluent-0.4.0/.claude/specs/decisions.md +244 -0
  7. agentfluent-0.4.0/.claude/specs/plan-100-mcp-assessment.md +630 -0
  8. agentfluent-0.4.0/.claude/specs/plan-116-mcp-extraction.md +322 -0
  9. agentfluent-0.4.0/.claude/specs/prd-glossary.md +136 -0
  10. agentfluent-0.4.0/.claude/specs/prd-mvp.md +326 -0
  11. agentfluent-0.4.0/.claude/specs/prd-v0.3.md +281 -0
  12. agentfluent-0.4.0/.claude/specs/research-update-2026-04-15.md +127 -0
  13. agentfluent-0.4.0/.claude/specs/v0.4-scope-review.md +150 -0
  14. agentfluent-0.4.0/.github/ISSUE_TEMPLATE/bug_report.yml +81 -0
  15. agentfluent-0.4.0/.github/ISSUE_TEMPLATE/feature_request.yml +41 -0
  16. agentfluent-0.4.0/.github/PULL_REQUEST_TEMPLATE.md +21 -0
  17. agentfluent-0.4.0/.github/dependabot.yml +35 -0
  18. agentfluent-0.4.0/.github/workflows/ci.yml +37 -0
  19. agentfluent-0.4.0/.github/workflows/claude-review.yml +65 -0
  20. agentfluent-0.4.0/.github/workflows/dependabot-auto-merge.yml +34 -0
  21. agentfluent-0.4.0/.github/workflows/release-please.yml +76 -0
  22. agentfluent-0.4.0/.github/workflows/security-review.yml +68 -0
  23. agentfluent-0.4.0/.gitignore +40 -0
  24. agentfluent-0.4.0/.python-version +1 -0
  25. agentfluent-0.4.0/.release-please-manifest.json +3 -0
  26. agentfluent-0.4.0/CHANGELOG.md +108 -0
  27. agentfluent-0.4.0/CLAUDE.md +319 -0
  28. agentfluent-0.4.0/CONTRIBUTING.md +105 -0
  29. {agentfluent-0.2.0 → agentfluent-0.4.0}/PKG-INFO +134 -76
  30. {agentfluent-0.2.0 → agentfluent-0.4.0}/README.md +122 -65
  31. agentfluent-0.4.0/SECURITY.md +24 -0
  32. agentfluent-0.4.0/docs/AGENT_ANALYTICS_RESEARCH.md +475 -0
  33. agentfluent-0.4.0/docs/GLOSSARY.md +820 -0
  34. agentfluent-0.4.0/docs/SECURITY.md +151 -0
  35. agentfluent-0.4.0/docs/codefluent_cli_review_042526.md +215 -0
  36. agentfluent-0.4.0/images/archive/v0.3/demo-analyze.svg +346 -0
  37. agentfluent-0.4.0/images/archive/v0.3/demo-config-check.svg +119 -0
  38. agentfluent-0.4.0/images/archive/v0.3/demo-diagnostics.svg +327 -0
  39. agentfluent-0.4.0/images/archive/v0.3/demo-subagents.svg +187 -0
  40. agentfluent-0.4.0/images/demo-analyze.svg +346 -0
  41. agentfluent-0.4.0/images/demo-config-check.svg +119 -0
  42. agentfluent-0.4.0/images/demo-diagnostics.svg +327 -0
  43. agentfluent-0.4.0/images/demo-subagents.svg +187 -0
  44. {agentfluent-0.2.0 → agentfluent-0.4.0}/pyproject.toml +21 -3
  45. agentfluent-0.4.0/release-please-config.json +15 -0
  46. agentfluent-0.4.0/scripts/calibration/README.md +84 -0
  47. agentfluent-0.4.0/scripts/calibration/build_notebook.py +658 -0
  48. agentfluent-0.4.0/scripts/calibration/threshold_validation.ipynb +1396 -0
  49. agentfluent-0.4.0/scripts/generate_glossary_md.py +28 -0
  50. agentfluent-0.4.0/scripts/generate_readme_screenshots.py +182 -0
  51. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/agents/extractor.py +16 -12
  52. agentfluent-0.4.0/src/agentfluent/agents/models.py +107 -0
  53. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/agent_metrics.py +31 -6
  54. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/pipeline.py +72 -2
  55. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/pricing.py +4 -0
  56. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/commands/analyze.py +82 -11
  57. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/commands/config_check.py +17 -2
  58. agentfluent-0.4.0/src/agentfluent/cli/commands/explain.py +188 -0
  59. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/commands/list_cmd.py +38 -17
  60. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/formatters/helpers.py +22 -0
  61. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/formatters/table.py +180 -39
  62. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/main.py +42 -1
  63. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/__init__.py +6 -1
  64. agentfluent-0.4.0/src/agentfluent/config/mcp_discovery.py +334 -0
  65. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/models.py +43 -1
  66. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/scanner.py +2 -1
  67. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/core/discovery.py +8 -2
  68. agentfluent-0.4.0/src/agentfluent/core/parser.py +375 -0
  69. agentfluent-0.4.0/src/agentfluent/core/paths.py +90 -0
  70. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/core/session.py +29 -0
  71. agentfluent-0.4.0/src/agentfluent/diagnostics/__init__.py +7 -0
  72. agentfluent-0.4.0/src/agentfluent/diagnostics/aggregation.py +159 -0
  73. agentfluent-0.4.0/src/agentfluent/diagnostics/builtin_actions.py +77 -0
  74. agentfluent-0.4.0/src/agentfluent/diagnostics/correlator.py +650 -0
  75. agentfluent-0.4.0/src/agentfluent/diagnostics/delegation.py +528 -0
  76. agentfluent-0.4.0/src/agentfluent/diagnostics/mcp_assessment.py +346 -0
  77. agentfluent-0.4.0/src/agentfluent/diagnostics/model_routing.py +337 -0
  78. agentfluent-0.4.0/src/agentfluent/diagnostics/models.py +284 -0
  79. agentfluent-0.4.0/src/agentfluent/diagnostics/pipeline.py +275 -0
  80. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/diagnostics/signals.py +8 -3
  81. agentfluent-0.4.0/src/agentfluent/diagnostics/trace_signals.py +361 -0
  82. agentfluent-0.4.0/src/agentfluent/glossary/__init__.py +26 -0
  83. agentfluent-0.4.0/src/agentfluent/glossary/loader.py +157 -0
  84. agentfluent-0.4.0/src/agentfluent/glossary/models.py +85 -0
  85. agentfluent-0.4.0/src/agentfluent/glossary/render.py +136 -0
  86. agentfluent-0.4.0/src/agentfluent/glossary/terms.yaml +653 -0
  87. agentfluent-0.4.0/src/agentfluent/py.typed +0 -0
  88. agentfluent-0.4.0/src/agentfluent/traces/__init__.py +0 -0
  89. agentfluent-0.4.0/src/agentfluent/traces/discovery.py +75 -0
  90. agentfluent-0.4.0/src/agentfluent/traces/linker.py +50 -0
  91. agentfluent-0.4.0/src/agentfluent/traces/models.py +119 -0
  92. agentfluent-0.4.0/src/agentfluent/traces/parser.py +207 -0
  93. agentfluent-0.4.0/src/agentfluent/traces/retry.py +91 -0
  94. agentfluent-0.4.0/tests/__init__.py +0 -0
  95. agentfluent-0.4.0/tests/_builders.py +208 -0
  96. agentfluent-0.4.0/tests/conftest.py +141 -0
  97. agentfluent-0.4.0/tests/fixtures/.gitkeep +0 -0
  98. agentfluent-0.4.0/tests/fixtures/agents/empty_prompt.md +12 -0
  99. agentfluent-0.4.0/tests/fixtures/agents/no_frontmatter.md +4 -0
  100. agentfluent-0.4.0/tests/fixtures/agents/no_tools.md +8 -0
  101. agentfluent-0.4.0/tests/fixtures/agents/vague_description.md +9 -0
  102. agentfluent-0.4.0/tests/fixtures/agents/well_configured.md +41 -0
  103. agentfluent-0.4.0/tests/fixtures/mcp/claude_user_only.json +15 -0
  104. agentfluent-0.4.0/tests/fixtures/mcp/claude_user_with_disabled.json +14 -0
  105. agentfluent-0.4.0/tests/fixtures/session_basic.jsonl +4 -0
  106. agentfluent-0.4.0/tests/fixtures/session_block_per_line.jsonl +7 -0
  107. agentfluent-0.4.0/tests/fixtures/session_malformed.jsonl +3 -0
  108. agentfluent-0.4.0/tests/fixtures/session_skip_types.jsonl +8 -0
  109. agentfluent-0.4.0/tests/fixtures/session_streaming_dupes.jsonl +8 -0
  110. agentfluent-0.4.0/tests/fixtures/session_with_agent.jsonl +5 -0
  111. agentfluent-0.4.0/tests/fixtures/session_with_tool_calls.jsonl +5 -0
  112. agentfluent-0.4.0/tests/fixtures/subagents/agent-basic.jsonl +8 -0
  113. agentfluent-0.4.0/tests/fixtures/subagents/agent-empty.jsonl +0 -0
  114. agentfluent-0.4.0/tests/fixtures/subagents/agent-errors.jsonl +5 -0
  115. agentfluent-0.4.0/tests/fixtures/subagents/agent-large.jsonl +46 -0
  116. agentfluent-0.4.0/tests/fixtures/subagents/agent-malformed.jsonl +7 -0
  117. agentfluent-0.4.0/tests/fixtures/subagents/agent-retry.jsonl +7 -0
  118. agentfluent-0.4.0/tests/fixtures/subagents/agent-streaming-dupes.jsonl +6 -0
  119. agentfluent-0.4.0/tests/fixtures/subagents/agent-stuck.jsonl +11 -0
  120. agentfluent-0.4.0/tests/integration/__init__.py +0 -0
  121. agentfluent-0.4.0/tests/integration/test_agent_extraction.py +63 -0
  122. agentfluent-0.4.0/tests/integration/test_analytics.py +94 -0
  123. agentfluent-0.4.0/tests/integration/test_config_assessment.py +54 -0
  124. agentfluent-0.4.0/tests/integration/test_diagnostics.py +69 -0
  125. agentfluent-0.4.0/tests/integration/test_real_sessions.py +135 -0
  126. agentfluent-0.4.0/tests/integration/test_subagent_traces.py +180 -0
  127. agentfluent-0.4.0/tests/unit/__init__.py +0 -0
  128. agentfluent-0.4.0/tests/unit/cli/__init__.py +0 -0
  129. agentfluent-0.4.0/tests/unit/cli/conftest.py +163 -0
  130. agentfluent-0.4.0/tests/unit/cli/test_claude_config_dir.py +196 -0
  131. agentfluent-0.4.0/tests/unit/cli/test_cost_labeling.py +73 -0
  132. agentfluent-0.4.0/tests/unit/cli/test_deep_diagnostics_formatting.py +259 -0
  133. agentfluent-0.4.0/tests/unit/cli/test_diagnostics_default.py +126 -0
  134. agentfluent-0.4.0/tests/unit/cli/test_diagnostics_smoke.py +110 -0
  135. agentfluent-0.4.0/tests/unit/cli/test_exit_codes.py +108 -0
  136. agentfluent-0.4.0/tests/unit/cli/test_explain.py +98 -0
  137. agentfluent-0.4.0/tests/unit/cli/test_glossary_footer.py +127 -0
  138. agentfluent-0.4.0/tests/unit/cli/test_help.py +46 -0
  139. agentfluent-0.4.0/tests/unit/cli/test_json_alias.py +112 -0
  140. agentfluent-0.4.0/tests/unit/cli/test_json_output.py +104 -0
  141. agentfluent-0.4.0/tests/unit/cli/test_output_modes.py +73 -0
  142. agentfluent-0.4.0/tests/unit/cli/test_parse_warnings.py +39 -0
  143. agentfluent-0.4.0/tests/unit/test_agent_metrics.py +239 -0
  144. agentfluent-0.4.0/tests/unit/test_agent_models.py +88 -0
  145. agentfluent-0.4.0/tests/unit/test_correlator.py +612 -0
  146. agentfluent-0.4.0/tests/unit/test_dedup.py +262 -0
  147. agentfluent-0.4.0/tests/unit/test_delegation.py +492 -0
  148. agentfluent-0.4.0/tests/unit/test_delegation_yaml_draft.py +91 -0
  149. agentfluent-0.4.0/tests/unit/test_diagnostics.py +93 -0
  150. agentfluent-0.4.0/tests/unit/test_diagnostics_pipeline.py +515 -0
  151. agentfluent-0.4.0/tests/unit/test_discovery.py +169 -0
  152. agentfluent-0.4.0/tests/unit/test_extractor.py +299 -0
  153. agentfluent-0.4.0/tests/unit/test_glossary_drift.py +34 -0
  154. agentfluent-0.4.0/tests/unit/test_glossary_loader.py +230 -0
  155. agentfluent-0.4.0/tests/unit/test_glossary_render.py +103 -0
  156. agentfluent-0.4.0/tests/unit/test_mcp_assessment.py +455 -0
  157. agentfluent-0.4.0/tests/unit/test_mcp_discovery.py +554 -0
  158. agentfluent-0.4.0/tests/unit/test_model_routing.py +396 -0
  159. agentfluent-0.4.0/tests/unit/test_parser.py +497 -0
  160. agentfluent-0.4.0/tests/unit/test_paths.py +41 -0
  161. agentfluent-0.4.0/tests/unit/test_pipeline.py +72 -0
  162. agentfluent-0.4.0/tests/unit/test_pricing.py +141 -0
  163. agentfluent-0.4.0/tests/unit/test_recommendation_aggregation.py +310 -0
  164. agentfluent-0.4.0/tests/unit/test_scanner.py +107 -0
  165. agentfluent-0.4.0/tests/unit/test_scoring.py +212 -0
  166. agentfluent-0.4.0/tests/unit/test_session_models.py +191 -0
  167. agentfluent-0.4.0/tests/unit/test_signals.py +198 -0
  168. agentfluent-0.4.0/tests/unit/test_smoke.py +44 -0
  169. agentfluent-0.4.0/tests/unit/test_tokens.py +242 -0
  170. agentfluent-0.4.0/tests/unit/test_tools.py +102 -0
  171. agentfluent-0.4.0/tests/unit/test_trace_signals.py +309 -0
  172. agentfluent-0.4.0/tests/unit/test_traces_discovery.py +143 -0
  173. agentfluent-0.4.0/tests/unit/test_traces_fixtures.py +204 -0
  174. agentfluent-0.4.0/tests/unit/test_traces_linker.py +298 -0
  175. agentfluent-0.4.0/tests/unit/test_traces_models.py +318 -0
  176. agentfluent-0.4.0/tests/unit/test_traces_parser.py +460 -0
  177. agentfluent-0.4.0/tests/unit/test_traces_retry.py +310 -0
  178. agentfluent-0.4.0/uv.lock +2660 -0
  179. agentfluent-0.2.0/src/agentfluent/agents/models.py +0 -71
  180. agentfluent-0.2.0/src/agentfluent/core/parser.py +0 -255
  181. agentfluent-0.2.0/src/agentfluent/diagnostics/__init__.py +0 -51
  182. agentfluent-0.2.0/src/agentfluent/diagnostics/correlator.py +0 -248
  183. agentfluent-0.2.0/src/agentfluent/diagnostics/models.py +0 -75
  184. /agentfluent-0.2.0/src/agentfluent/agents/__init__.py → /agentfluent-0.4.0/.claude/specs/.gitkeep +0 -0
  185. {agentfluent-0.2.0 → agentfluent-0.4.0}/LICENSE +0 -0
  186. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/__init__.py +0 -0
  187. {agentfluent-0.2.0/src/agentfluent/analytics → agentfluent-0.4.0/src/agentfluent/agents}/__init__.py +0 -0
  188. {agentfluent-0.2.0/src/agentfluent/cli → agentfluent-0.4.0/src/agentfluent/analytics}/__init__.py +0 -0
  189. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/tokens.py +0 -0
  190. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/analytics/tools.py +0 -0
  191. {agentfluent-0.2.0/src/agentfluent/cli/commands → agentfluent-0.4.0/src/agentfluent/cli}/__init__.py +0 -0
  192. {agentfluent-0.2.0/src/agentfluent/cli/formatters → agentfluent-0.4.0/src/agentfluent/cli/commands}/__init__.py +0 -0
  193. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/exit_codes.py +0 -0
  194. {agentfluent-0.2.0/src/agentfluent/core → agentfluent-0.4.0/src/agentfluent/cli/formatters}/__init__.py +0 -0
  195. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/cli/formatters/json_output.py +0 -0
  196. {agentfluent-0.2.0 → agentfluent-0.4.0}/src/agentfluent/config/scoring.py +0 -0
  197. /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
+ }