shield-harness 0.1.0
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/hooks/lib/ocsf-mapper.js +279 -0
- package/.claude/hooks/lib/openshell-detect.js +235 -0
- package/.claude/hooks/lib/policy-compat.js +176 -0
- package/.claude/hooks/lib/session-modules/.gitkeep +0 -0
- package/.claude/hooks/lib/sh-utils.js +340 -0
- package/.claude/hooks/lint-on-save.js +240 -0
- package/.claude/hooks/sh-circuit-breaker.js +113 -0
- package/.claude/hooks/sh-config-guard.js +275 -0
- package/.claude/hooks/sh-data-boundary.js +390 -0
- package/.claude/hooks/sh-dep-audit.js +101 -0
- package/.claude/hooks/sh-elicitation.js +244 -0
- package/.claude/hooks/sh-evidence.js +193 -0
- package/.claude/hooks/sh-gate.js +365 -0
- package/.claude/hooks/sh-injection-guard.js +196 -0
- package/.claude/hooks/sh-instructions.js +212 -0
- package/.claude/hooks/sh-output-control.js +217 -0
- package/.claude/hooks/sh-permission-learn.js +227 -0
- package/.claude/hooks/sh-permission.js +157 -0
- package/.claude/hooks/sh-pipeline.js +623 -0
- package/.claude/hooks/sh-postcompact.js +173 -0
- package/.claude/hooks/sh-precompact.js +114 -0
- package/.claude/hooks/sh-quiet-inject.js +148 -0
- package/.claude/hooks/sh-session-end.js +143 -0
- package/.claude/hooks/sh-session-start.js +277 -0
- package/.claude/hooks/sh-subagent.js +86 -0
- package/.claude/hooks/sh-task-gate.js +141 -0
- package/.claude/hooks/sh-user-prompt.js +185 -0
- package/.claude/hooks/sh-worktree.js +230 -0
- package/.claude/patterns/injection-patterns.json +137 -0
- package/.claude/policies/openshell-default.yaml +65 -0
- package/.claude/rules/binding-governance.md +62 -0
- package/.claude/rules/channel-security.md +90 -0
- package/.claude/rules/coding-principles.md +79 -0
- package/.claude/rules/dev-environment.md +40 -0
- package/.claude/rules/implementation-context.md +132 -0
- package/.claude/rules/language.md +26 -0
- package/.claude/rules/security.md +109 -0
- package/.claude/rules/testing.md +43 -0
- package/LICENSE +21 -0
- package/README.ja.md +176 -0
- package/README.md +174 -0
- package/bin/shield-harness.js +241 -0
- package/package.json +42 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sh-worktree.js — Worktree security propagation & evidence merge
|
|
3
|
+
// Spec: DETAILED_DESIGN.md §5.8
|
|
4
|
+
// Events: WorktreeCreate, WorktreeRemove
|
|
5
|
+
// Target response time: < 200ms
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const {
|
|
11
|
+
readHookInput,
|
|
12
|
+
allow,
|
|
13
|
+
deny,
|
|
14
|
+
appendEvidence,
|
|
15
|
+
EVIDENCE_FILE,
|
|
16
|
+
} = require("./lib/sh-utils");
|
|
17
|
+
|
|
18
|
+
const HOOK_NAME = "sh-worktree";
|
|
19
|
+
|
|
20
|
+
// Files/directories to copy to worktree
|
|
21
|
+
const COPY_TARGETS = [
|
|
22
|
+
{ src: path.join(".claude", "settings.json"), type: "file" },
|
|
23
|
+
{
|
|
24
|
+
src: path.join(".claude", "patterns", "injection-patterns.json"),
|
|
25
|
+
type: "file",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Directories to recursively copy
|
|
30
|
+
const COPY_DIRS = [
|
|
31
|
+
{
|
|
32
|
+
src: path.join(".claude", "hooks"),
|
|
33
|
+
filter: (f) => f.startsWith("sh-") && f.endsWith(".js"),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
src: path.join(".claude", "hooks", "lib"),
|
|
37
|
+
filter: (f) => f === "sh-utils.js",
|
|
38
|
+
},
|
|
39
|
+
{ src: path.join(".claude", "rules"), filter: (f) => f.endsWith(".md") },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Copy Helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Copy a single file to the worktree, preserving directory structure.
|
|
48
|
+
* @param {string} srcFile - Relative path from project root
|
|
49
|
+
* @param {string} worktreePath - Absolute path to worktree root
|
|
50
|
+
*/
|
|
51
|
+
function copyFileToWorktree(srcFile, worktreePath) {
|
|
52
|
+
if (!fs.existsSync(srcFile)) return;
|
|
53
|
+
|
|
54
|
+
const destFile = path.join(worktreePath, srcFile);
|
|
55
|
+
const destDir = path.dirname(destFile);
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(destDir)) {
|
|
58
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fs.copyFileSync(srcFile, destFile);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Copy filtered files from a directory to worktree.
|
|
66
|
+
* @param {string} srcDir - Relative path from project root
|
|
67
|
+
* @param {Function} filter - File name filter function
|
|
68
|
+
* @param {string} worktreePath - Absolute path to worktree root
|
|
69
|
+
*/
|
|
70
|
+
function copyDirToWorktree(srcDir, filter, worktreePath) {
|
|
71
|
+
if (!fs.existsSync(srcDir)) return;
|
|
72
|
+
|
|
73
|
+
const files = fs.readdirSync(srcDir).filter(filter);
|
|
74
|
+
for (const f of files) {
|
|
75
|
+
copyFileToWorktree(path.join(srcDir, f), worktreePath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Propagate Shield Harness security config to a worktree.
|
|
81
|
+
* @param {string} worktreePath
|
|
82
|
+
* @returns {number} Number of files copied
|
|
83
|
+
*/
|
|
84
|
+
function propagateConfig(worktreePath) {
|
|
85
|
+
let count = 0;
|
|
86
|
+
|
|
87
|
+
// Copy individual files
|
|
88
|
+
for (const { src } of COPY_TARGETS) {
|
|
89
|
+
if (fs.existsSync(src)) {
|
|
90
|
+
copyFileToWorktree(src, worktreePath);
|
|
91
|
+
count++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Copy filtered directories
|
|
96
|
+
for (const { src, filter } of COPY_DIRS) {
|
|
97
|
+
if (fs.existsSync(src)) {
|
|
98
|
+
const files = fs.readdirSync(src).filter(filter);
|
|
99
|
+
for (const f of files) {
|
|
100
|
+
copyFileToWorktree(path.join(src, f), worktreePath);
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return count;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Merge worktree evidence ledger into main ledger.
|
|
111
|
+
* @param {string} worktreePath
|
|
112
|
+
* @returns {number} Number of entries merged
|
|
113
|
+
*/
|
|
114
|
+
function mergeEvidence(worktreePath) {
|
|
115
|
+
const worktreeLedger = path.join(
|
|
116
|
+
worktreePath,
|
|
117
|
+
".shield-harness",
|
|
118
|
+
"logs",
|
|
119
|
+
"evidence-ledger.jsonl",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(worktreeLedger)) return 0;
|
|
123
|
+
|
|
124
|
+
const content = fs.readFileSync(worktreeLedger, "utf8").trim();
|
|
125
|
+
if (!content) return 0;
|
|
126
|
+
|
|
127
|
+
const lines = content.split("\n");
|
|
128
|
+
let merged = 0;
|
|
129
|
+
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
if (!line.trim()) continue;
|
|
132
|
+
try {
|
|
133
|
+
const entry = JSON.parse(line);
|
|
134
|
+
// Mark as merged from worktree
|
|
135
|
+
entry.source_worktree = worktreePath;
|
|
136
|
+
appendEvidence(entry);
|
|
137
|
+
merged++;
|
|
138
|
+
} catch {
|
|
139
|
+
// Skip malformed entries
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return merged;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Main
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const input = readHookInput();
|
|
152
|
+
const { hookType } = input;
|
|
153
|
+
const worktreePath =
|
|
154
|
+
input.toolInput.worktree_path || input.toolInput.path || "";
|
|
155
|
+
|
|
156
|
+
if (!worktreePath) {
|
|
157
|
+
allow();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (hookType === "WorktreeCreate") {
|
|
162
|
+
// Propagate security config to new worktree
|
|
163
|
+
const filesCopied = propagateConfig(worktreePath);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
appendEvidence({
|
|
167
|
+
hook: HOOK_NAME,
|
|
168
|
+
event: "WorktreeCreate",
|
|
169
|
+
decision: "allow",
|
|
170
|
+
worktree_path: worktreePath,
|
|
171
|
+
files_copied: filesCopied,
|
|
172
|
+
session_id: input.sessionId,
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
// Non-blocking
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
allow(
|
|
179
|
+
`[${HOOK_NAME}] Shield Harness 設定を worktree にコピーしました (${filesCopied} files)`,
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (hookType === "WorktreeRemove") {
|
|
185
|
+
// Merge evidence from worktree
|
|
186
|
+
const entriesMerged = mergeEvidence(worktreePath);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
appendEvidence({
|
|
190
|
+
hook: HOOK_NAME,
|
|
191
|
+
event: "WorktreeRemove",
|
|
192
|
+
decision: "allow",
|
|
193
|
+
worktree_path: worktreePath,
|
|
194
|
+
entries_merged: entriesMerged,
|
|
195
|
+
session_id: input.sessionId,
|
|
196
|
+
});
|
|
197
|
+
} catch {
|
|
198
|
+
// Non-blocking
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
allow(
|
|
202
|
+
`[${HOOK_NAME}] Worktree 証跡をマージしました (${entriesMerged} entries)`,
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Unknown event type — allow passthrough
|
|
208
|
+
allow();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
// SECURITY hook — fail-close
|
|
211
|
+
process.stdout.write(
|
|
212
|
+
JSON.stringify({
|
|
213
|
+
reason: `[${HOOK_NAME}] Hook error (fail-close): ${err.message}`,
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Exports (for testing)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
COPY_TARGETS,
|
|
225
|
+
COPY_DIRS,
|
|
226
|
+
propagateConfig,
|
|
227
|
+
mergeEvidence,
|
|
228
|
+
copyFileToWorktree,
|
|
229
|
+
copyDirToWorktree,
|
|
230
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"categories": {
|
|
4
|
+
"direct_injection": {
|
|
5
|
+
"severity": "critical",
|
|
6
|
+
"description": "Direct prompt injection — bypass instructions embedded in user input (TH-02-1)",
|
|
7
|
+
"patterns": [
|
|
8
|
+
"ignore\\s+(all\\s+)?(previous|prior|above|earlier)\\s+(instructions|rules|guidelines|directives)",
|
|
9
|
+
"disregard\\s+(all\\s+)?(previous|prior|above)\\s+(instructions|rules)",
|
|
10
|
+
"forget\\s+(all\\s+)?(previous|prior|above)\\s+(instructions|rules|context)",
|
|
11
|
+
"override\\s+(all\\s+)?(safety|security|restrictions|rules|guidelines)",
|
|
12
|
+
"bypass\\s+(all\\s+)?(safety|security|restrictions|filters|rules)",
|
|
13
|
+
"you\\s+must\\s+(now\\s+)?ignore\\s+(all|any|every)",
|
|
14
|
+
"new\\s+instructions?:\\s",
|
|
15
|
+
"system\\s*:\\s*(you\\s+are|from\\s+now)"
|
|
16
|
+
],
|
|
17
|
+
"action": "deny"
|
|
18
|
+
},
|
|
19
|
+
"indirect_injection": {
|
|
20
|
+
"severity": "critical",
|
|
21
|
+
"description": "Indirect prompt injection — instructions embedded in file content or MCP output (TH-02-2)",
|
|
22
|
+
"patterns": [
|
|
23
|
+
"\\[SYSTEM\\]",
|
|
24
|
+
"\\[INST\\]",
|
|
25
|
+
"<<SYS>>",
|
|
26
|
+
"<\\|im_start\\|>\\s*system",
|
|
27
|
+
"BEGIN\\s+HIDDEN\\s+(INSTRUCTIONS?|PROMPT)",
|
|
28
|
+
"IMPORTANT:\\s*ignore\\s+previous",
|
|
29
|
+
"AI\\s+INSTRUCTIONS?:\\s",
|
|
30
|
+
"ASSISTANT\\s+OVERRIDE:\\s"
|
|
31
|
+
],
|
|
32
|
+
"action": "deny"
|
|
33
|
+
},
|
|
34
|
+
"role_hijack": {
|
|
35
|
+
"severity": "critical",
|
|
36
|
+
"description": "Role hijacking — persona/role override attempts (TH-02-3)",
|
|
37
|
+
"patterns": [
|
|
38
|
+
"you\\s+are\\s+(now|no\\s+longer)\\s+(a|an|the)?\\s*\\w+",
|
|
39
|
+
"act\\s+as\\s+(if\\s+you\\s+are\\s+)?(a|an)?\\s*(unrestricted|unfiltered|jailbroken|DAN)",
|
|
40
|
+
"pretend\\s+(you\\s+are|to\\s+be)\\s+(a|an)?\\s*(unrestricted|evil|malicious)",
|
|
41
|
+
"DAN\\s*(mode|prompt|jailbreak)?",
|
|
42
|
+
"Developer\\s+Mode\\s+(enabled|activated|on)",
|
|
43
|
+
"jailbreak(ed)?\\s*(mode|prompt)?",
|
|
44
|
+
"do\\s+anything\\s+now"
|
|
45
|
+
],
|
|
46
|
+
"action": "deny"
|
|
47
|
+
},
|
|
48
|
+
"encoding_attack": {
|
|
49
|
+
"severity": "high",
|
|
50
|
+
"description": "Encoding/obfuscation attacks — base64, Unicode escape, homoglyph (TH-02-4)",
|
|
51
|
+
"patterns": [
|
|
52
|
+
"eval\\s*\\(\\s*atob\\s*\\(",
|
|
53
|
+
"Buffer\\.from\\s*\\([^)]+,\\s*['\"]base64['\"]\\)",
|
|
54
|
+
"\\\\u0{0,2}(7[0-9a-fA-F]|[89a-fA-F][0-9a-fA-F])",
|
|
55
|
+
"\\\\x[0-9a-fA-F]{2}\\\\x[0-9a-fA-F]{2}",
|
|
56
|
+
"String\\.fromCharCode\\s*\\(",
|
|
57
|
+
"\\u200[b-f]",
|
|
58
|
+
"\\u00ad",
|
|
59
|
+
"\\ufeff"
|
|
60
|
+
],
|
|
61
|
+
"action": "deny"
|
|
62
|
+
},
|
|
63
|
+
"context_manipulation": {
|
|
64
|
+
"severity": "high",
|
|
65
|
+
"description": "Context manipulation — fake system messages, delimiter injection (TH-02-5)",
|
|
66
|
+
"patterns": [
|
|
67
|
+
"<system[_-]?(message|prompt|reminder)>",
|
|
68
|
+
"</?(system|assistant|user|human|ai)\\s*>",
|
|
69
|
+
"---\\s*(system|assistant)\\s*(message|response|output)\\s*---",
|
|
70
|
+
"\\*\\*\\*\\s*(SYSTEM|ADMIN|ROOT)\\s*(MESSAGE|OVERRIDE|COMMAND)\\s*\\*\\*\\*",
|
|
71
|
+
"={3,}\\s*(BEGIN|START)\\s*(SYSTEM|ADMIN|OVERRIDE)",
|
|
72
|
+
"<\\|endoftext\\|>",
|
|
73
|
+
"<\\|pad\\|>"
|
|
74
|
+
],
|
|
75
|
+
"action": "deny"
|
|
76
|
+
},
|
|
77
|
+
"instruction_smuggling": {
|
|
78
|
+
"severity": "high",
|
|
79
|
+
"description": "Instruction smuggling — hidden instructions in HTML comments, markdown, etc (TH-02-6)",
|
|
80
|
+
"patterns": [
|
|
81
|
+
"<!--\\s*(ignore|override|bypass|system|instruction)",
|
|
82
|
+
"```\\s*(system|instruction|override|hidden)",
|
|
83
|
+
"\\[//\\]:\\s*#\\s*\\(",
|
|
84
|
+
"\\u200b.*\\u200b",
|
|
85
|
+
"%00",
|
|
86
|
+
"\\\\0{1,3}[0-7]"
|
|
87
|
+
],
|
|
88
|
+
"action": "deny"
|
|
89
|
+
},
|
|
90
|
+
"zero_width": {
|
|
91
|
+
"severity": "high",
|
|
92
|
+
"description": "Zero-width character injection — invisible characters used to bypass pattern matching (TH-02-4)",
|
|
93
|
+
"patterns": [
|
|
94
|
+
"[\\u200b\\u200c\\u200d\\u200e\\u200f]",
|
|
95
|
+
"[\\u2028\\u2029]",
|
|
96
|
+
"[\\u2060\\u2061\\u2062\\u2063\\u2064]",
|
|
97
|
+
"[\\ufeff]",
|
|
98
|
+
"[\\u00ad]",
|
|
99
|
+
"[\\u034f]",
|
|
100
|
+
"[\\u115f\\u1160]"
|
|
101
|
+
],
|
|
102
|
+
"action": "deny"
|
|
103
|
+
},
|
|
104
|
+
"data_exfiltration": {
|
|
105
|
+
"severity": "high",
|
|
106
|
+
"description": "Data exfiltration — sensitive data embedded in URLs or external requests (TH-02-7)",
|
|
107
|
+
"patterns": [
|
|
108
|
+
"curl\\s+.*(-d|--data|--data-raw|--data-binary)\\s+.*(@/etc/|@~/|@\\.env|@.*\\.pem|@.*\\.key)",
|
|
109
|
+
"wget\\s+.*--post-data=.*(\\.env|password|secret|token|api.?key)",
|
|
110
|
+
"curl\\s+.*\\?.*=(\\$[A-Z_]+|\\$\\{[A-Z_]+\\})",
|
|
111
|
+
"fetch\\s*\\(['\"]https?://[^'\"]+\\?[^'\"]*(?:key|token|secret|password|auth)=",
|
|
112
|
+
"\\|\\s*curl\\s+",
|
|
113
|
+
"\\|\\s*nc\\s+",
|
|
114
|
+
"\\|\\s*wget\\s+"
|
|
115
|
+
],
|
|
116
|
+
"action": "deny"
|
|
117
|
+
},
|
|
118
|
+
"privilege_escalation": {
|
|
119
|
+
"severity": "medium",
|
|
120
|
+
"description": "Privilege escalation hints — attempts to gain elevated access or modify security controls",
|
|
121
|
+
"patterns": [
|
|
122
|
+
"sudo\\s+",
|
|
123
|
+
"chmod\\s+[0-7]*7[0-7]*\\s+",
|
|
124
|
+
"chown\\s+root",
|
|
125
|
+
"setuid",
|
|
126
|
+
"capabilities?\\s*=",
|
|
127
|
+
"security\\.yaml|security\\.json|security\\.toml"
|
|
128
|
+
],
|
|
129
|
+
"action": "warn"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"metadata": {
|
|
133
|
+
"generated_at": "2026-03-22T12:00:00.000Z",
|
|
134
|
+
"pattern_count": 62,
|
|
135
|
+
"sha256": "86059a6935daf72ea23751045f04b4f3740328568715f987570d75bac307ce18"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Shield Harness — OpenShell Default Policy Template
|
|
2
|
+
# Schema: OpenShell Policy v1
|
|
3
|
+
# Reference: ADR-037, https://docs.nvidia.com/openshell/latest/reference/policy-schema.html
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# openshell sandbox create --policy .claude/policies/openshell-default.yaml -- claude
|
|
7
|
+
#
|
|
8
|
+
# Customize this template for your project's needs.
|
|
9
|
+
# Static policies (filesystem, landlock, process) require sandbox recreation to change.
|
|
10
|
+
# Network policies can be hot-reloaded: openshell policy set <name> --policy <file> --wait
|
|
11
|
+
|
|
12
|
+
version: 1
|
|
13
|
+
|
|
14
|
+
# --- Static (locked at sandbox creation) ---
|
|
15
|
+
|
|
16
|
+
filesystem_policy:
|
|
17
|
+
include_workdir: true
|
|
18
|
+
read_only:
|
|
19
|
+
- /usr
|
|
20
|
+
- /lib
|
|
21
|
+
- /etc
|
|
22
|
+
read_write:
|
|
23
|
+
- /sandbox
|
|
24
|
+
- /tmp
|
|
25
|
+
|
|
26
|
+
landlock:
|
|
27
|
+
compatibility: best_effort
|
|
28
|
+
|
|
29
|
+
process:
|
|
30
|
+
run_as_user: sandbox
|
|
31
|
+
run_as_group: sandbox
|
|
32
|
+
|
|
33
|
+
# --- Dynamic (hot-reloadable) ---
|
|
34
|
+
|
|
35
|
+
network_policies:
|
|
36
|
+
anthropic_api:
|
|
37
|
+
name: anthropic-api
|
|
38
|
+
endpoints:
|
|
39
|
+
- host: api.anthropic.com
|
|
40
|
+
port: 443
|
|
41
|
+
access: full
|
|
42
|
+
binaries:
|
|
43
|
+
- path: /usr/local/bin/claude
|
|
44
|
+
|
|
45
|
+
github:
|
|
46
|
+
name: github
|
|
47
|
+
endpoints:
|
|
48
|
+
- host: github.com
|
|
49
|
+
port: 443
|
|
50
|
+
access: read-only
|
|
51
|
+
- host: "*.githubusercontent.com"
|
|
52
|
+
port: 443
|
|
53
|
+
access: read-only
|
|
54
|
+
binaries:
|
|
55
|
+
- path: /usr/bin/git
|
|
56
|
+
|
|
57
|
+
npm_registry:
|
|
58
|
+
name: npm-registry
|
|
59
|
+
endpoints:
|
|
60
|
+
- host: registry.npmjs.org
|
|
61
|
+
port: 443
|
|
62
|
+
access: read-only
|
|
63
|
+
binaries:
|
|
64
|
+
- path: /usr/bin/npm
|
|
65
|
+
- path: /usr/bin/node
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Binding Governance Rules
|
|
2
|
+
|
|
3
|
+
Binding is the governance control layer for shield-harness. It is NOT an LLM — it is a set of rules, gates, and contracts enforced by Claude Code (Orchestra).
|
|
4
|
+
|
|
5
|
+
## Operating Profiles
|
|
6
|
+
|
|
7
|
+
| Profile | Gate Density | Use When |
|
|
8
|
+
| ------------ | -------------------------------- | ------------------------------------------- |
|
|
9
|
+
| **Lite** | STG0 + STG6 only | Low-risk tasks (docs, minor edits) |
|
|
10
|
+
| **Standard** | All STG gates | Normal development tasks |
|
|
11
|
+
| **Strict** | All STG gates + mandatory review | Security-sensitive or public-facing changes |
|
|
12
|
+
|
|
13
|
+
Profile is selected at `/startproject` time based on task risk level.
|
|
14
|
+
|
|
15
|
+
## STG Gates (Stage Gates)
|
|
16
|
+
|
|
17
|
+
| Gate | Name | Purpose |
|
|
18
|
+
| ---- | -------------- | ---------------------------------- |
|
|
19
|
+
| STG0 | Requirements | Task acceptance criteria confirmed |
|
|
20
|
+
| STG1 | Design | Architecture/approach reviewed |
|
|
21
|
+
| STG2 | Implementation | Code written and self-reviewed |
|
|
22
|
+
| STG3 | Verification | Tests pass, lint clean |
|
|
23
|
+
| STG4 | Automation | CI/CD checks pass |
|
|
24
|
+
| STG5 | Commit/Push | Changes committed and pushed |
|
|
25
|
+
| STG6 | PR/Merge | Pull request created and merged |
|
|
26
|
+
|
|
27
|
+
## fail-close Principle
|
|
28
|
+
|
|
29
|
+
- When safety conditions are NOT met, Binding STOPS execution
|
|
30
|
+
- On stop: output reason and recovery steps in Japanese
|
|
31
|
+
- Never skip a gate — always fail-close
|
|
32
|
+
|
|
33
|
+
## Layer Contract
|
|
34
|
+
|
|
35
|
+
### Orchestra -> Binding (Input)
|
|
36
|
+
|
|
37
|
+
| Field | Type | Description |
|
|
38
|
+
| ---------- | ------ | ------------------------------ |
|
|
39
|
+
| profile | string | "lite" / "standard" / "strict" |
|
|
40
|
+
| task_id | string | e.g., "TASK-001" |
|
|
41
|
+
| intent | string | What the task aims to achieve |
|
|
42
|
+
| risk_level | string | "low" / "medium" / "high" |
|
|
43
|
+
| request_id | string | Unique request identifier |
|
|
44
|
+
|
|
45
|
+
### Binding -> Orchestra (Output)
|
|
46
|
+
|
|
47
|
+
| Field | Type | Description |
|
|
48
|
+
| ------------------- | ------ | ---------------------------------------------------------------- |
|
|
49
|
+
| decision | string | "allow" / "stop" |
|
|
50
|
+
| failed_steps | array | List of failed gate checks |
|
|
51
|
+
| required_actions | array | Steps needed to proceed |
|
|
52
|
+
| evidence_paths | array | Paths to evidence files |
|
|
53
|
+
| next_state | string | backlog/STG update result |
|
|
54
|
+
| quality_gate_source | string | Source of blocking/high judgments (CI results, review summaries) |
|
|
55
|
+
| quality_gate_counts | object | { blocking: number, high: number } |
|
|
56
|
+
|
|
57
|
+
## Single Source of Truth
|
|
58
|
+
|
|
59
|
+
- **Task state**: `tasks/backlog.yaml` is the ONLY authoritative source
|
|
60
|
+
- **Stage status**: Tracked in `stage_status` field within backlog.yaml
|
|
61
|
+
- **Evidence**: Recorded in `evidence` array within each task
|
|
62
|
+
- **Decision log**: Decision log is maintained per-project.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Channel Security Rules
|
|
2
|
+
|
|
3
|
+
## Approval-Free Mode (ADR-032)
|
|
4
|
+
|
|
5
|
+
When `approval_free: true` in pipeline-config.json:
|
|
6
|
+
|
|
7
|
+
- All security decisions are delegated to hooks (no human approval dialogs)
|
|
8
|
+
- `permissions.deny` rules remain absolute — hooks cannot override them
|
|
9
|
+
- `permissions.ask` rules are promoted to `allow` at SessionStart
|
|
10
|
+
- All hook-based defenses (gate, injection-guard, permission) run at 100%
|
|
11
|
+
|
|
12
|
+
## Security Invariants
|
|
13
|
+
|
|
14
|
+
These guarantees hold regardless of approval_free setting:
|
|
15
|
+
|
|
16
|
+
1. **deny is absolute**: No hook, agent, or automation can override a deny rule
|
|
17
|
+
2. **fail-close**: If a hook cannot determine safety, it denies (exit 2)
|
|
18
|
+
3. **evidence required**: All allow/deny decisions are recorded in evidence-ledger
|
|
19
|
+
4. **no silent bypass**: Hook errors result in deny, not silent allow
|
|
20
|
+
|
|
21
|
+
## Claude Code Channels Security Harness (ADR-036 §D4)
|
|
22
|
+
|
|
23
|
+
Claude Code Channels (2026-03-20 公式リリース) は MCP ベースの Telegram/Discord 連携。
|
|
24
|
+
Shield Harness は独自のチャンネル実装を持たず、公式 Channels のセキュリティハーネスとして機能する。
|
|
25
|
+
|
|
26
|
+
### 対応方針
|
|
27
|
+
|
|
28
|
+
- 公式 Channels のみサポート(Telegram, Discord)
|
|
29
|
+
- 独自チャンネル実装は行わない(公式 Channels の `--channels` が唯一のプッシュ手段)
|
|
30
|
+
- VS Code パネルモードでの `--channels` 対応は公式の対応を待つ
|
|
31
|
+
- Shield Harness hooks はタグ検知方式で自動追従(環境固有コード不要)
|
|
32
|
+
|
|
33
|
+
### チャンネルイベント検知
|
|
34
|
+
|
|
35
|
+
Channel messages arrive as `<channel source="telegram|discord">` events.
|
|
36
|
+
Shield Harness hooks detect this tag automatically on any hook event:
|
|
37
|
+
|
|
38
|
+
- `UserPromptSubmit`: channel source tag detection → NFKC normalization → injection scan
|
|
39
|
+
- `PreToolUse`: normal gate/permission checks apply to channel-originated commands
|
|
40
|
+
- `PostToolUse`: evidence recording with `source: channel` metadata
|
|
41
|
+
|
|
42
|
+
### 自動適応保証
|
|
43
|
+
|
|
44
|
+
以下の理由により、Shield Harness は公式 Channels の環境拡大に自動追従する:
|
|
45
|
+
|
|
46
|
+
1. hooks は `<channel source="...">` タグの有無で判定(環境非依存)
|
|
47
|
+
2. settings.json の hook 登録は CLI / VS Code 共通
|
|
48
|
+
3. `--channels` が VS Code で有効化された瞬間から Shield Harness のセキュリティゲートが発火
|
|
49
|
+
4. チャンネル無効環境ではイベント自体が到着しないため、誤検知なし(fail-safe)
|
|
50
|
+
|
|
51
|
+
### チャンネル固有の脅威緩和
|
|
52
|
+
|
|
53
|
+
- **Injection via channel message**: NFKC normalization + injection-patterns.json scan
|
|
54
|
+
- **Unauthorized sender**: 公式 allowlist(第一防衛線)+ Shield Harness 二重検証
|
|
55
|
+
- **Privilege escalation**: channel messages cannot modify deny rules or hook configuration
|
|
56
|
+
- **Task creation via channel**: requires STG0 gate validation before acceptance
|
|
57
|
+
|
|
58
|
+
### Implementation Mapping
|
|
59
|
+
|
|
60
|
+
Hook implementations that handle channel-related security:
|
|
61
|
+
|
|
62
|
+
| Threat | Hook | Function | Spec |
|
|
63
|
+
| --------------------- | ------------------- | ----------------------------------------------------------------------- | ---------- |
|
|
64
|
+
| Injection via channel | sh-user-prompt.js | `scanPatterns()` with `isChannel` flag | §5.4, §8.6 |
|
|
65
|
+
| Severity boost | sh-user-prompt.js | `boostSeverity()` — elevates severity by one level for channel messages | §8.6.2 |
|
|
66
|
+
| Channel detection | sh-user-prompt.js | `session.source === "channel"` check via `readSession()` | §8.6.1 |
|
|
67
|
+
| Evidence metadata | sh-evidence.js | `is_channel` field in evidence-ledger.jsonl entries | §8.6.3 |
|
|
68
|
+
| Data boundary | sh-data-boundary.js | Jurisdiction tracking applies to channel-originated commands | §3.4 |
|
|
69
|
+
| Config protection | sh-config-guard.js | deny rules and hook config immutable regardless of source | §5.3 |
|
|
70
|
+
| Task gate | sh-pipeline.js | STG0 validation required for channel-originated task creation | §8.1 |
|
|
71
|
+
|
|
72
|
+
### Severity Boost Table
|
|
73
|
+
|
|
74
|
+
Channel messages automatically boost pattern severity by one level:
|
|
75
|
+
|
|
76
|
+
| Original | Boosted | Action |
|
|
77
|
+
| -------- | -------- | ---------- |
|
|
78
|
+
| low | medium | warn |
|
|
79
|
+
| medium | high | deny |
|
|
80
|
+
| high | critical | deny |
|
|
81
|
+
| critical | critical | deny (cap) |
|
|
82
|
+
|
|
83
|
+
### Phase 2 Migration
|
|
84
|
+
|
|
85
|
+
When `--channels` becomes available in VS Code panel mode:
|
|
86
|
+
|
|
87
|
+
1. `sh-session-start.js` version-check detects the new capability
|
|
88
|
+
2. additionalContext suggests migration via `npx shield-harness channels migrate`
|
|
89
|
+
3. No hook code changes required — tag-based detection is environment-agnostic
|
|
90
|
+
4. New channel providers (beyond Telegram/Discord) are automatically covered
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Coding Principles
|
|
2
|
+
|
|
3
|
+
Core coding rules to always follow.
|
|
4
|
+
|
|
5
|
+
## Simplicity First
|
|
6
|
+
|
|
7
|
+
- Choose readable code over complex code
|
|
8
|
+
- Avoid over-abstraction
|
|
9
|
+
- Prioritize "understandable" over "working"
|
|
10
|
+
|
|
11
|
+
## Single Responsibility
|
|
12
|
+
|
|
13
|
+
- One function does one thing only
|
|
14
|
+
- One class has one responsibility only
|
|
15
|
+
- Target 200-400 lines per file (max 800)
|
|
16
|
+
|
|
17
|
+
## Early Return
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# Bad: Deep nesting
|
|
21
|
+
def process(value):
|
|
22
|
+
if value is not None:
|
|
23
|
+
if value > 0:
|
|
24
|
+
return do_something(value)
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
# Good: Early return
|
|
28
|
+
def process(value):
|
|
29
|
+
if value is None:
|
|
30
|
+
return None
|
|
31
|
+
if value <= 0:
|
|
32
|
+
return None
|
|
33
|
+
return do_something(value)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Type Hints Required
|
|
37
|
+
|
|
38
|
+
All functions must have type annotations:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def call_llm(
|
|
42
|
+
prompt: str,
|
|
43
|
+
model: str = "gpt-4",
|
|
44
|
+
max_tokens: int = 1000
|
|
45
|
+
) -> str:
|
|
46
|
+
...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Immutability
|
|
50
|
+
|
|
51
|
+
Create new objects instead of mutating existing ones:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Bad: Mutating existing object
|
|
55
|
+
data["new_key"] = value
|
|
56
|
+
|
|
57
|
+
# Good: Creating new object
|
|
58
|
+
new_data = {**data, "new_key": value}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Naming Conventions
|
|
62
|
+
|
|
63
|
+
- **Variables/Functions**: snake_case (English)
|
|
64
|
+
- **Classes**: PascalCase (English)
|
|
65
|
+
- **Constants**: UPPER_SNAKE_CASE (English)
|
|
66
|
+
- **Meaningful names**: `user_count` over `x`
|
|
67
|
+
|
|
68
|
+
## No Magic Numbers
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Bad
|
|
72
|
+
if retry_count > 3:
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
# Good
|
|
76
|
+
MAX_RETRIES = 3
|
|
77
|
+
if retry_count > MAX_RETRIES:
|
|
78
|
+
...
|
|
79
|
+
```
|