@drawbridge/drawbridge-agents 0.1.97 → 0.1.98
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-template/settings.json +9 -0
- package/README.md +3 -0
- package/claude/CLAUDE.md +1 -0
- package/conventions/secrets.md +34 -0
- package/hooks/guard-secrets.js +82 -0
- package/package.json +1 -1
|
@@ -56,6 +56,15 @@
|
|
|
56
56
|
},
|
|
57
57
|
"hooks": {
|
|
58
58
|
"PreToolUse": [
|
|
59
|
+
{
|
|
60
|
+
"matcher": "Read|Bash",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "node ./.claude/hooks/guard-secrets.js"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
59
68
|
{
|
|
60
69
|
"matcher": "Write|Edit|MultiEdit|NotebookEdit",
|
|
61
70
|
"hooks": [
|
package/README.md
CHANGED
|
@@ -112,6 +112,9 @@ identical copy via the sync mirror (see "Delivery model").
|
|
|
112
112
|
doc anchor and on `@story`-anchored code changed without a drawbridge-docs change (once per
|
|
113
113
|
turn; the second attempt passes so the agent must answer), and **nudges** (non-blocking) when
|
|
114
114
|
the graph looks stale. It skips plan-mode and turns with no source changes.
|
|
115
|
+
- **Secrets guard.** `hooks/guard-secrets.js` (PreToolUse on Read/Bash) denies a Read of any
|
|
116
|
+
`.env*` file and a Bash command that displays one, prints the environment, or prints an `op`
|
|
117
|
+
value without a redirect. See `conventions/secrets.md`.
|
|
115
118
|
- **Read-before-write hooks.** `hooks/consult-docs-record.js` (PostToolUse on Read/Bash) remembers
|
|
116
119
|
that a drawbridge-docs page was read and a graph query ran in this session;
|
|
117
120
|
`hooks/consult-docs-gate.js` (PreToolUse on Write/Edit) refuses the first source edit until
|
package/claude/CLAUDE.md
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Never put a secret in the transcript
|
|
2
|
+
|
|
3
|
+
Nothing an agent prints, reads or echoes may carry a secret. The family's env files
|
|
4
|
+
(`.env.local`, `.env.development`, `.env.production`, drawbridge-tests' `.env`) hold a 1Password
|
|
5
|
+
service-account token and, in tests, live keys. A transcript is copied, summarized and shared,
|
|
6
|
+
so a value that lands in one has to be rotated.
|
|
7
|
+
|
|
8
|
+
The api's 1Password service-account token reached a session transcript twice, the second time
|
|
9
|
+
on 2026-10-28. Darren: "That is the second time you have done that. You cannot fucking keep doing
|
|
10
|
+
that." This page and `hooks/guard-secrets.js` exist so there is no third time.
|
|
11
|
+
|
|
12
|
+
## The rule
|
|
13
|
+
|
|
14
|
+
- Never `Read`, `cat`, `head`, `tail`, `less`, `grep`, `sed`, `awk` or `source` a `.env*` file.
|
|
15
|
+
Not to "see which Environment it names", not to copy one line, not to check it exists (`ls` and
|
|
16
|
+
`test -f` are fine; they show no content).
|
|
17
|
+
- Never print `process.env`, `printenv`, bare `env`, or a value read from `op` (`op read`,
|
|
18
|
+
`op item get`, `op environment read`) to the console. Those commands are allowed only with
|
|
19
|
+
their output redirected to a file that nothing then displays.
|
|
20
|
+
- Never paste a key, token, connection string, cookie or session value into a reply, a commit
|
|
21
|
+
message, an Asana comment, a doc or a script's console output.
|
|
22
|
+
- A script that needs an env value runs under the repo's `scripts/op.sh`
|
|
23
|
+
(`ENV_FILE=.env.local sh scripts/op.sh node <script>`) and consumes the value inside the
|
|
24
|
+
process. `op run` conceals secret-looking strings in console output, which is why ids are
|
|
25
|
+
written to files and read back; it is not a licence to display the env file.
|
|
26
|
+
- Copying an env file between checkouts (a worktree without `.env.development`) is `cp` with no
|
|
27
|
+
display, and the copy is deleted when the work is done.
|
|
28
|
+
- If a secret does reach the transcript, say so at once, in one line, and name what to rotate.
|
|
29
|
+
|
|
30
|
+
## Enforced
|
|
31
|
+
|
|
32
|
+
`hooks/guard-secrets.js` is a PreToolUse hook on `Read` and `Bash`, mirrored into every repo's
|
|
33
|
+
`.claude/hooks/`. It denies a `Read` of any `.env*` file and a Bash command that displays one,
|
|
34
|
+
prints the environment, or prints an `op` value without a redirect. The denial names this page.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook (Read|Bash): nothing displays a secret. The family's env files carry a
|
|
3
|
+
// 1Password service-account token, and that token reached a session transcript twice
|
|
4
|
+
// (the second time 2026-10-28). A convention did not stop it, so this does: a Read of any
|
|
5
|
+
// .env* file is denied, and so is a Bash command that displays one, prints the environment,
|
|
6
|
+
// or prints an `op` value to the console. See conventions/secrets.md.
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
const readStdin = () => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(0, 'utf8') || '{}')
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const deny = (reason) => {
|
|
19
|
+
process.stdout.write(JSON.stringify({
|
|
20
|
+
hookSpecificOutput: {
|
|
21
|
+
hookEventName: 'PreToolUse',
|
|
22
|
+
permissionDecision: 'deny',
|
|
23
|
+
permissionDecisionReason: reason
|
|
24
|
+
}
|
|
25
|
+
}))
|
|
26
|
+
process.exit(0)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SEE = ' See conventions/secrets.md: run the script under scripts/op.sh and let it consume the value; never display it.'
|
|
30
|
+
|
|
31
|
+
// `.env`, `.env.local`, `.env.development`, `.env.production`, `.env.example` is harmless but
|
|
32
|
+
// treated the same: nothing needs to read any of them.
|
|
33
|
+
const isEnvFile = (filePath) => /^\.env(\..+)?$/.test(path.basename(filePath))
|
|
34
|
+
|
|
35
|
+
// A command whose argument list names an env file, by a tool that prints file content.
|
|
36
|
+
const DISPLAYS_FILE = /(^|[|;&(\s])(cat|head|tail|less|more|bat|grep|rg|egrep|fgrep|sed|awk|cut|sort|uniq|strings|xxd|od|source|\.)\s+(?:[^|;&]*[\s/'"=])?\.env(\.[\w-]+)?(['"\s]|$)/m
|
|
37
|
+
|
|
38
|
+
// The whole environment, printed.
|
|
39
|
+
const PRINTS_ENV = /(^|[|;&(\s])(printenv|env)(\s*$|\s*[|;&)]|\s+-)/m
|
|
40
|
+
|
|
41
|
+
// An `op` read whose output is not sent to a file.
|
|
42
|
+
const OP_READ = /(^|[|;&(\s])op\s+(read|item\s+get|environment\s+read)\b/m
|
|
43
|
+
|
|
44
|
+
const run = () => {
|
|
45
|
+
const input = readStdin()
|
|
46
|
+
const tool = input.tool_name || ''
|
|
47
|
+
const toolInput = input.tool_input || {}
|
|
48
|
+
|
|
49
|
+
if (tool === 'Read') {
|
|
50
|
+
const filePath = toolInput.file_path || ''
|
|
51
|
+
if (filePath && isEnvFile(filePath)) {
|
|
52
|
+
deny('Blocked: `' + path.basename(filePath) + '` holds a secret and is never read into the transcript.' + SEE)
|
|
53
|
+
}
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tool !== 'Bash') process.exit(0)
|
|
58
|
+
|
|
59
|
+
const command = String(toolInput.command || '')
|
|
60
|
+
if (!command) process.exit(0)
|
|
61
|
+
|
|
62
|
+
if (DISPLAYS_FILE.test(command)) {
|
|
63
|
+
deny('Blocked: this command displays a .env file, which holds a secret.' + SEE)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (PRINTS_ENV.test(command)) {
|
|
67
|
+
deny('Blocked: this command prints the whole environment, secrets included.' + SEE)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (OP_READ.test(command) && !/>\s*\S/.test(command)) {
|
|
71
|
+
deny('Blocked: an `op` read prints a secret to the console. Redirect it to a file nothing displays, or run the consumer under scripts/op.sh.' + SEE)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
run()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// Never break a user's turn because of a hook bug — fail open.
|
|
81
|
+
process.exit(0)
|
|
82
|
+
}
|
package/package.json
CHANGED