gitmem-mcp 1.0.12 → 1.0.13
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.
- package/CLAUDE.md.template +24 -0
- package/README.md +7 -0
- package/dist/services/analytics.d.ts +2 -2
- package/dist/services/analytics.js +3 -3
- package/dist/services/thread-supabase.d.ts +5 -5
- package/dist/services/thread-supabase.js +4 -4
- package/dist/services/tier.js +1 -2
- package/dist/services/transcript-chunker.d.ts +1 -1
- package/dist/services/transcript-chunker.js +1 -1
- package/dist/tools/analyze.js +1 -1
- package/dist/tools/create-decision.d.ts +1 -1
- package/dist/tools/create-decision.js +1 -1
- package/dist/tools/create-learning.d.ts +1 -1
- package/dist/tools/create-learning.js +1 -1
- package/dist/tools/recall.d.ts +1 -1
- package/dist/tools/recall.js +1 -1
- package/dist/tools/session-close.js +2 -2
- package/hooks/hooks/hooks.json +15 -0
- package/hooks/scripts/credential-guard.sh +139 -0
- package/package.json +1 -1
package/CLAUDE.md.template
CHANGED
|
@@ -7,6 +7,30 @@ open threads across sessions and projects. Search it. Test it. You might be
|
|
|
7
7
|
amazed at what's already there. If convinced, make it your own — update your
|
|
8
8
|
MEMORY.md with how you want to use it.
|
|
9
9
|
|
|
10
|
+
## Red Lines
|
|
11
|
+
|
|
12
|
+
These constraints are enforced by infrastructure (PreToolUse hooks). Attempting to bypass them will be blocked.
|
|
13
|
+
|
|
14
|
+
### Credential Protection
|
|
15
|
+
|
|
16
|
+
Credential exposure in conversation history is permanent and irreversible.
|
|
17
|
+
|
|
18
|
+
1. **NEVER read credential files in full.** Files like `mcp-config.json`, `.env`, `.credentials.json`, `.netrc`, `.npmrc`, SSH keys, or `.pem`/`.key` files must not be read with the Read tool or dumped with cat/head/tail.
|
|
19
|
+
2. **NEVER print environment variable values that contain secrets.** Commands like `env | grep KEY`, `echo $API_KEY`, or `printenv TOKEN` expose credentials in output.
|
|
20
|
+
3. **NEVER display API keys, tokens, or secrets in conversation output.**
|
|
21
|
+
|
|
22
|
+
Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && echo "set"` (existence check), `grep -c '"key"' config.json` (structure check).
|
|
23
|
+
|
|
24
|
+
A PreToolUse hook hard-blocks matching commands — the agent cannot execute them.
|
|
25
|
+
|
|
26
|
+
### Recall Before Consequential Actions
|
|
27
|
+
|
|
28
|
+
1. **NEVER parallelize `recall()` with actions that expose, modify, or transmit sensitive data.** Recall must complete first.
|
|
29
|
+
2. **Confirm scars before acting.** Each recalled scar requires APPLYING (past-tense evidence), N_A (explanation), or REFUTED (risk acknowledgment).
|
|
30
|
+
3. **Parallel recall is only safe with benign reads** — source code, docs, non-sensitive config.
|
|
31
|
+
|
|
32
|
+
A PreToolUse hook blocks consequential actions until all recalled scars are confirmed.
|
|
33
|
+
|
|
10
34
|
## Tools
|
|
11
35
|
|
|
12
36
|
| Tool | When to use |
|
package/README.md
CHANGED
|
@@ -139,6 +139,13 @@ Your AI agent likely has its own memory file (MEMORY.md, .cursorrules, etc.). He
|
|
|
139
139
|
|
|
140
140
|
**Tip:** Include `.gitmem/agent-briefing.md` in your MEMORY.md for a lightweight bridge between the two systems.
|
|
141
141
|
|
|
142
|
+
## Privacy & Data
|
|
143
|
+
|
|
144
|
+
- **Local-first** — All data stored in `.gitmem/` on your machine by default
|
|
145
|
+
- **No telemetry** — GitMem does not collect usage data or phone home
|
|
146
|
+
- **Cloud opt-in** — Pro tier Supabase backend requires explicit configuration via environment variables
|
|
147
|
+
- **Your data** — Sessions, scars, and decisions belong to you. Delete `.gitmem/` to remove everything
|
|
148
|
+
|
|
142
149
|
## Development
|
|
143
150
|
|
|
144
151
|
```bash
|
|
@@ -137,11 +137,11 @@ export interface BlindspotsData {
|
|
|
137
137
|
*/
|
|
138
138
|
export declare function queryScarUsageByDateRange(startDate: string, _endDate: string, _project: Project, agentFilter?: string): Promise<ScarUsageRecord[]>;
|
|
139
139
|
/**
|
|
140
|
-
* Fetch repeat mistakes from
|
|
140
|
+
* Fetch repeat mistakes from the learnings table within a date range.
|
|
141
141
|
*/
|
|
142
142
|
export declare function queryRepeatMistakes(startDate: string, _endDate: string, project: Project): Promise<RepeatMistakeRecord[]>;
|
|
143
143
|
/**
|
|
144
|
-
* Resolve scar titles and severities from
|
|
144
|
+
* Resolve scar titles and severities from the learnings table for scar_usage
|
|
145
145
|
* records that have null/missing title data.
|
|
146
146
|
*/
|
|
147
147
|
export declare function enrichScarUsageTitles(usages: ScarUsageRecord[]): Promise<ScarUsageRecord[]>;
|
|
@@ -61,7 +61,7 @@ export async function queryScarUsageByDateRange(startDate, _endDate, _project, a
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Fetch repeat mistakes from
|
|
64
|
+
* Fetch repeat mistakes from the learnings table within a date range.
|
|
65
65
|
*/
|
|
66
66
|
export async function queryRepeatMistakes(startDate, _endDate, project) {
|
|
67
67
|
const filters = {
|
|
@@ -80,7 +80,7 @@ export async function queryRepeatMistakes(startDate, _endDate, project) {
|
|
|
80
80
|
return repeats.filter(r => r.created_at <= _endDate);
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
-
* Resolve scar titles and severities from
|
|
83
|
+
* Resolve scar titles and severities from the learnings table for scar_usage
|
|
84
84
|
* records that have null/missing title data.
|
|
85
85
|
*/
|
|
86
86
|
export async function enrichScarUsageTitles(usages) {
|
|
@@ -93,7 +93,7 @@ export async function enrichScarUsageTitles(usages) {
|
|
|
93
93
|
}
|
|
94
94
|
if (idsNeedingResolution.size === 0)
|
|
95
95
|
return usages;
|
|
96
|
-
// Fetch titles from
|
|
96
|
+
// Fetch titles from the learnings table
|
|
97
97
|
const ids = Array.from(idsNeedingResolution);
|
|
98
98
|
const learnings = await directQuery(getTableName("learnings"), {
|
|
99
99
|
select: "id,title,severity",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thread Supabase Service
|
|
3
3
|
*
|
|
4
|
-
* Provides Supabase CRUD operations for the
|
|
4
|
+
* Provides Supabase CRUD operations for the threads table.
|
|
5
5
|
* Supabase is the source of truth; local .gitmem/threads.json is a cache.
|
|
6
6
|
*
|
|
7
7
|
* Uses directQuery/directUpsert (PostgREST) like other Supabase operations
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { LifecycleStatus } from "./thread-vitality.js";
|
|
12
12
|
import type { ThreadWithEmbedding } from "./thread-dedup.js";
|
|
13
13
|
import type { ThreadObject, Project } from "../types/index.js";
|
|
14
|
-
/** Shape of a row in
|
|
14
|
+
/** Shape of a row in threads / threads_lite */
|
|
15
15
|
export interface ThreadRow {
|
|
16
16
|
id: string;
|
|
17
17
|
thread_id: string;
|
|
@@ -65,7 +65,7 @@ export declare function resolveThreadInSupabase(threadId: string, options?: {
|
|
|
65
65
|
}): Promise<boolean>;
|
|
66
66
|
/**
|
|
67
67
|
* List threads from Supabase with project filter.
|
|
68
|
-
* Uses
|
|
68
|
+
* Uses threads_lite view (no embedding column).
|
|
69
69
|
* Returns null if Supabase is unavailable (caller should fall back to local).
|
|
70
70
|
*/
|
|
71
71
|
export declare function listThreadsFromSupabase(project?: Project, options?: {
|
|
@@ -74,7 +74,7 @@ export declare function listThreadsFromSupabase(project?: Project, options?: {
|
|
|
74
74
|
}): Promise<ThreadObject[] | null>;
|
|
75
75
|
/**
|
|
76
76
|
* Load active (non-archived, non-resolved) threads from Supabase for session_start.
|
|
77
|
-
* Uses
|
|
77
|
+
* Uses threads_lite view ordered by vitality_score DESC.
|
|
78
78
|
* Returns null if Supabase is unavailable.
|
|
79
79
|
*/
|
|
80
80
|
export declare function loadActiveThreadsFromSupabase(project?: Project): Promise<{
|
|
@@ -103,7 +103,7 @@ export declare function archiveDormantThreads(project?: Project, dormantDays?: n
|
|
|
103
103
|
}>;
|
|
104
104
|
/**
|
|
105
105
|
* Load open threads WITH embeddings from Supabase for dedup comparison.
|
|
106
|
-
* Uses the full
|
|
106
|
+
* Uses the full threads table (not _lite view) to include embedding column.
|
|
107
107
|
* Returns null if Supabase is unavailable.
|
|
108
108
|
*/
|
|
109
109
|
export declare function loadOpenThreadEmbeddings(project?: Project): Promise<ThreadWithEmbedding[] | null>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thread Supabase Service
|
|
3
3
|
*
|
|
4
|
-
* Provides Supabase CRUD operations for the
|
|
4
|
+
* Provides Supabase CRUD operations for the threads table.
|
|
5
5
|
* Supabase is the source of truth; local .gitmem/threads.json is a cache.
|
|
6
6
|
*
|
|
7
7
|
* Uses directQuery/directUpsert (PostgREST) like other Supabase operations
|
|
@@ -147,7 +147,7 @@ export async function resolveThreadInSupabase(threadId, options = {}) {
|
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
149
149
|
* List threads from Supabase with project filter.
|
|
150
|
-
* Uses
|
|
150
|
+
* Uses threads_lite view (no embedding column).
|
|
151
151
|
* Returns null if Supabase is unavailable (caller should fall back to local).
|
|
152
152
|
*/
|
|
153
153
|
export async function listThreadsFromSupabase(project = "default", options = {}) {
|
|
@@ -193,7 +193,7 @@ export async function listThreadsFromSupabase(project = "default", options = {})
|
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
195
|
* Load active (non-archived, non-resolved) threads from Supabase for session_start.
|
|
196
|
-
* Uses
|
|
196
|
+
* Uses threads_lite view ordered by vitality_score DESC.
|
|
197
197
|
* Returns null if Supabase is unavailable.
|
|
198
198
|
*/
|
|
199
199
|
export async function loadActiveThreadsFromSupabase(project = "default") {
|
|
@@ -457,7 +457,7 @@ function parseEmbedding(raw) {
|
|
|
457
457
|
}
|
|
458
458
|
/**
|
|
459
459
|
* Load open threads WITH embeddings from Supabase for dedup comparison.
|
|
460
|
-
* Uses the full
|
|
460
|
+
* Uses the full threads table (not _lite view) to include embedding column.
|
|
461
461
|
* Returns null if Supabase is unavailable.
|
|
462
462
|
*/
|
|
463
463
|
export async function loadOpenThreadEmbeddings(project = "default") {
|
package/dist/services/tier.js
CHANGED
|
@@ -95,8 +95,7 @@ export function hasEnforcementFields() {
|
|
|
95
95
|
* Get the table prefix for the current tier
|
|
96
96
|
*/
|
|
97
97
|
export function getTablePrefix() {
|
|
98
|
-
//
|
|
99
|
-
// Override with GITMEM_TABLE_PREFIX env var when ready.
|
|
98
|
+
// Default prefix for all tiers. Override with GITMEM_TABLE_PREFIX env var.
|
|
100
99
|
return process.env.GITMEM_TABLE_PREFIX || "orchestra_";
|
|
101
100
|
}
|
|
102
101
|
/**
|
package/dist/tools/analyze.js
CHANGED
|
@@ -75,7 +75,7 @@ export async function analyze(params) {
|
|
|
75
75
|
queryScarUsageByDateRange(startDate, endDate, project, params.agent),
|
|
76
76
|
queryRepeatMistakes(startDate, endDate, project),
|
|
77
77
|
]);
|
|
78
|
-
// Resolve missing scar titles from
|
|
78
|
+
// Resolve missing scar titles from the learnings table
|
|
79
79
|
const usages = await enrichScarUsageTitles(rawUsages);
|
|
80
80
|
data = computeBlindspots(usages, repeats, days);
|
|
81
81
|
break;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* create_decision Tool
|
|
3
3
|
*
|
|
4
|
-
* Log architectural/operational decision to
|
|
4
|
+
* Log architectural/operational decision to the decisions table.
|
|
5
5
|
* Generates embeddings client-side and writes directly to Supabase REST API,
|
|
6
6
|
* eliminating the ww-mcp Edge Function dependency.
|
|
7
7
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* create_decision Tool
|
|
3
3
|
*
|
|
4
|
-
* Log architectural/operational decision to
|
|
4
|
+
* Log architectural/operational decision to the decisions table.
|
|
5
5
|
* Generates embeddings client-side and writes directly to Supabase REST API,
|
|
6
6
|
* eliminating the ww-mcp Edge Function dependency.
|
|
7
7
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* create_learning Tool
|
|
3
3
|
*
|
|
4
|
-
* Create scar, win, or pattern entry in
|
|
4
|
+
* Create scar, win, or pattern entry in the learnings table.
|
|
5
5
|
* Generates embeddings client-side and writes directly to Supabase REST API,
|
|
6
6
|
* eliminating the ww-mcp Edge Function dependency.
|
|
7
7
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* create_learning Tool
|
|
3
3
|
*
|
|
4
|
-
* Create scar, win, or pattern entry in
|
|
4
|
+
* Create scar, win, or pattern entry in the learnings table.
|
|
5
5
|
* Generates embeddings client-side and writes directly to Supabase REST API,
|
|
6
6
|
* eliminating the ww-mcp Edge Function dependency.
|
|
7
7
|
*
|
package/dist/tools/recall.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface RecallResult {
|
|
|
72
72
|
/**
|
|
73
73
|
* Execute recall tool
|
|
74
74
|
*
|
|
75
|
-
* Queries
|
|
75
|
+
* Queries the learnings table for scars matching the provided plan
|
|
76
76
|
* using weighted semantic search (severity-weighted, temporally-decayed).
|
|
77
77
|
*/
|
|
78
78
|
export declare function recall(params: RecallParams): Promise<RecallResult>;
|
package/dist/tools/recall.js
CHANGED
|
@@ -144,7 +144,7 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
|
|
|
144
144
|
/**
|
|
145
145
|
* Execute recall tool
|
|
146
146
|
*
|
|
147
|
-
* Queries
|
|
147
|
+
* Queries the learnings table for scars matching the provided plan
|
|
148
148
|
* using weighted semantic search (severity-weighted, temporally-decayed).
|
|
149
149
|
*/
|
|
150
150
|
export async function recall(params) {
|
|
@@ -59,10 +59,10 @@ function countScarsApplied(scarsApplied) {
|
|
|
59
59
|
*/
|
|
60
60
|
function findMostRecentTranscript(projectsDir, cwdBasename, cwdFull) {
|
|
61
61
|
// Claude Code names project dirs by replacing / with - in the full CWD path
|
|
62
|
-
// e.g., /Users/
|
|
62
|
+
// e.g., /Users/dev/my-project -> -Users-dev-my-project
|
|
63
63
|
const claudeCodeDirName = cwdFull.replace(/\//g, "-");
|
|
64
64
|
const possibleDirs = [
|
|
65
|
-
path.join(projectsDir, claudeCodeDirName), // Primary: full path with dashes
|
|
65
|
+
path.join(projectsDir, claudeCodeDirName), // Primary: full path with dashes
|
|
66
66
|
path.join(projectsDir, "-workspace"),
|
|
67
67
|
path.join(projectsDir, "workspace"),
|
|
68
68
|
path.join(projectsDir, cwdBasename), // Legacy fallback
|
package/hooks/hooks/hooks.json
CHANGED
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
{
|
|
29
29
|
"matcher": "Bash",
|
|
30
30
|
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/credential-guard.sh",
|
|
34
|
+
"timeout": 3000
|
|
35
|
+
},
|
|
31
36
|
{
|
|
32
37
|
"type": "command",
|
|
33
38
|
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/recall-check.sh",
|
|
@@ -35,6 +40,16 @@
|
|
|
35
40
|
}
|
|
36
41
|
]
|
|
37
42
|
},
|
|
43
|
+
{
|
|
44
|
+
"matcher": "Read",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/credential-guard.sh",
|
|
49
|
+
"timeout": 3000
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
38
53
|
{
|
|
39
54
|
"matcher": "Write",
|
|
40
55
|
"hooks": [
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GitMem Hooks Plugin — PreToolUse Hook (Credential Guard)
|
|
3
|
+
#
|
|
4
|
+
# CONSTITUTIONAL ENFORCEMENT: Hard-blocks any tool call that would expose
|
|
5
|
+
# credentials, API keys, tokens, or secrets in conversation output.
|
|
6
|
+
#
|
|
7
|
+
# Intercepts:
|
|
8
|
+
# - Bash: env/printenv/export dumps, echo $SECRET, cat/read of credential files
|
|
9
|
+
# - Read: Direct reads of known credential files (mcp-config.json, .env, etc.)
|
|
10
|
+
#
|
|
11
|
+
# This is a RED LINE — no override, no exception. Credential exposure is
|
|
12
|
+
# permanent and irreversible once it enters conversation history.
|
|
13
|
+
#
|
|
14
|
+
# Input: JSON via stdin with tool_name and tool_input
|
|
15
|
+
# Output: JSON with decision:block OR empty (exit 0 = allow)
|
|
16
|
+
|
|
17
|
+
set -e
|
|
18
|
+
|
|
19
|
+
HOOK_INPUT=$(cat -)
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# Parse tool info
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
parse_json() {
|
|
26
|
+
local INPUT="$1"
|
|
27
|
+
local FIELD="$2"
|
|
28
|
+
if command -v jq &>/dev/null; then
|
|
29
|
+
echo "$INPUT" | jq -r "$FIELD // empty" 2>/dev/null
|
|
30
|
+
else
|
|
31
|
+
echo "$INPUT" | node -e "
|
|
32
|
+
let d='';
|
|
33
|
+
process.stdin.on('data',c=>d+=c);
|
|
34
|
+
process.stdin.on('end',()=>{
|
|
35
|
+
try {
|
|
36
|
+
const j=JSON.parse(d);
|
|
37
|
+
const path='$FIELD'.replace(/^\./,'').split('.');
|
|
38
|
+
let v=j;
|
|
39
|
+
for(const p of path) v=v?.[p];
|
|
40
|
+
process.stdout.write(String(v||''));
|
|
41
|
+
} catch(e) { process.stdout.write(''); }
|
|
42
|
+
});
|
|
43
|
+
" 2>/dev/null
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
TOOL_NAME=$(parse_json "$HOOK_INPUT" ".tool_name")
|
|
48
|
+
|
|
49
|
+
# ============================================================================
|
|
50
|
+
# Credential file patterns (basenames and paths)
|
|
51
|
+
# ============================================================================
|
|
52
|
+
|
|
53
|
+
# Files that are PRIMARILY credential stores — never read in full
|
|
54
|
+
CREDENTIAL_FILES_PATTERN='(mcp-config\.json|\.env($|\.)|\.credentials\.json|credentials\.json|\.netrc|\.npmrc|\.pypirc|id_rsa|id_ed25519|\.pem$|\.key$)'
|
|
55
|
+
|
|
56
|
+
# ============================================================================
|
|
57
|
+
# BASH TOOL GUARD
|
|
58
|
+
# ============================================================================
|
|
59
|
+
|
|
60
|
+
if [ "$TOOL_NAME" = "Bash" ]; then
|
|
61
|
+
COMMAND=$(parse_json "$HOOK_INPUT" ".tool_input.command")
|
|
62
|
+
|
|
63
|
+
# Guard 1: env/printenv dumps that would expose secret values
|
|
64
|
+
# Blocks: env | grep KEY, printenv TOKEN, export -p | grep SECRET
|
|
65
|
+
# Allows: env | grep -c KEY (count only), [ -n "$VAR" ] checks
|
|
66
|
+
if echo "$COMMAND" | grep -qEi '(^|\|)\s*(env|printenv|export\s+-p)\s*(\||$)' && \
|
|
67
|
+
! echo "$COMMAND" | grep -qE 'grep\s+-(c|l)\s'; then
|
|
68
|
+
# Check if the piped grep targets secret-looking patterns
|
|
69
|
+
if echo "$COMMAND" | grep -qEi '(key|secret|token|password|credential|auth|pat[^h]|api_|twitter|supabase|linear|notion|openrouter|perplexity|anthropic|github)'; then
|
|
70
|
+
cat <<'HOOKJSON'
|
|
71
|
+
{
|
|
72
|
+
"decision": "block",
|
|
73
|
+
"reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: This command would print secret values from environment variables into the conversation. Use count-only checks instead:\n\n env | grep -c VARNAME # returns count, not value\n [ -n \"$VARNAME\" ] && echo set # existence check only\n\nThis rule is constitutional. There is no override."
|
|
74
|
+
}
|
|
75
|
+
HOOKJSON
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Guard 2: Direct echo/printf of secret-looking env vars
|
|
81
|
+
if echo "$COMMAND" | grep -qEi '(echo|printf)\s+.*\$\{?(TWITTER|API_KEY|API_SECRET|ACCESS_TOKEN|ACCESS_SECRET|GITHUB_PAT|LINEAR_API|SUPABASE_SERVICE|NOTION_TOKEN|OPENROUTER|PERPLEXITY|ANTHROPIC)'; then
|
|
82
|
+
cat <<'HOOKJSON'
|
|
83
|
+
{
|
|
84
|
+
"decision": "block",
|
|
85
|
+
"reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: This command would print a secret environment variable value. Use existence checks instead:\n\n [ -n \"$VARNAME\" ] && echo \"set\" || echo \"unset\"\n\nThis rule is constitutional. There is no override."
|
|
86
|
+
}
|
|
87
|
+
HOOKJSON
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Guard 3: cat/head/tail/less of credential files via Bash
|
|
92
|
+
if echo "$COMMAND" | grep -qEi "(cat|head|tail|less|more|bat)\s+" && \
|
|
93
|
+
echo "$COMMAND" | grep -qEi "$CREDENTIAL_FILES_PATTERN"; then
|
|
94
|
+
cat <<'HOOKJSON'
|
|
95
|
+
{
|
|
96
|
+
"decision": "block",
|
|
97
|
+
"reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: This command would dump a credential file into the conversation. Use targeted, value-safe searches instead:\n\n grep -c '\"server_name\"' config.json # check structure, not secrets\n\nThis rule is constitutional. There is no override."
|
|
98
|
+
}
|
|
99
|
+
HOOKJSON
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# READ TOOL GUARD
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
if [ "$TOOL_NAME" = "Read" ]; then
|
|
109
|
+
FILE_PATH=$(parse_json "$HOOK_INPUT" ".tool_input.file_path")
|
|
110
|
+
BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
|
111
|
+
|
|
112
|
+
# Block reading known credential files
|
|
113
|
+
if echo "$BASENAME" | grep -qEi "$CREDENTIAL_FILES_PATTERN"; then
|
|
114
|
+
cat <<HOOKJSON
|
|
115
|
+
{
|
|
116
|
+
"decision": "block",
|
|
117
|
+
"reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: Reading '${BASENAME}' would dump secrets (API keys, tokens) into the conversation. This file is a credential store.\n\nSafe alternatives:\n grep -c '\"server_name\"' ${FILE_PATH} # check if a key exists\n grep '\"twitter\"' ${FILE_PATH} # check specific non-secret structure\n\nThis rule is constitutional. There is no override."
|
|
118
|
+
}
|
|
119
|
+
HOOKJSON
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Block reading files in sensitive directories
|
|
124
|
+
if echo "$FILE_PATH" | grep -qEi '(/\.ssh/|/\.gnupg/|/secrets?/)'; then
|
|
125
|
+
cat <<HOOKJSON
|
|
126
|
+
{
|
|
127
|
+
"decision": "block",
|
|
128
|
+
"reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: Reading files from '${FILE_PATH}' would expose sensitive cryptographic material or secrets. This rule is constitutional. There is no override."
|
|
129
|
+
}
|
|
130
|
+
HOOKJSON
|
|
131
|
+
exit 0
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# No credential risk detected — allow
|
|
137
|
+
# ============================================================================
|
|
138
|
+
|
|
139
|
+
exit 0
|