@hasna/hooks 0.5.0 → 0.6.1
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/README.md +81 -8
- package/bin/index.js +1756 -382
- package/bin/serve.js +5258 -0
- package/dist/cf/provision.d.ts +24 -0
- package/dist/config.d.ts +18 -0
- package/dist/db/legacy-import.d.ts +1 -1
- package/dist/db/migrations/004_hooks_table.d.ts +9 -0
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/storage-sync.d.ts +26 -6
- package/dist/index.d.ts +18 -2
- package/dist/index.js +5351 -286
- package/dist/lib/custom-install.d.ts +19 -0
- package/dist/lib/manifest.d.ts +70 -0
- package/dist/lib/resolve.d.ts +19 -0
- package/dist/lib/run.d.ts +37 -0
- package/dist/lib/store.d.ts +69 -0
- package/dist/lib/sync.d.ts +35 -0
- package/dist/serve.d.ts +36 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +133 -42
- package/hooks/codewith-native-common.test.ts +15 -2
- package/hooks/hook-scanoutput/README.md +151 -0
- package/hooks/hook-scanoutput/package.json +12 -0
- package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
- package/hooks/hook-scanoutput/src/hook.ts +319 -0
- package/hooks/hook-workspace-repos-guard/README.md +63 -0
- package/hooks/hook-workspace-repos-guard/package.json +12 -0
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +312 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +352 -0
- package/hooks/hook-workspace-repos-guard/tsconfig.json +21 -0
- package/hooks/mention-context/README.md +109 -0
- package/hooks/mention-context/package.json +9 -0
- package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
- package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
- package/hooks/mention-context/src/hook.test.ts +68 -0
- package/hooks/mention-context/src/test_run_capture.py +345 -0
- package/package.json +9 -5
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: scanoutput
|
|
5
|
+
*
|
|
6
|
+
* PostToolUse hook that scans tool output for credential shapes and WARNS.
|
|
7
|
+
*
|
|
8
|
+
* THIS IS DETECTION, NOT PREVENTION, AND THE DISTINCTION IS THE WHOLE DESIGN.
|
|
9
|
+
*
|
|
10
|
+
* PostToolUse fires after the tool has run and after its result exists. Measured
|
|
11
|
+
* across the 48-hook catalog at @hasna/hooks 0.5.0: no output-rewriting field
|
|
12
|
+
* exists on any event — `continue`, `decision`, `hookSpecificOutput`,
|
|
13
|
+
* `additionalContext`, `suppressOutput` and `permissionDecision` are the whole
|
|
14
|
+
* surface, and `suppressOutput` hides THE HOOK'S OWN stdout rather than the
|
|
15
|
+
* tool's. So by the time this hook can see a credential, that credential is
|
|
16
|
+
* already in the transcript. This hook cannot and does not stop that.
|
|
17
|
+
*
|
|
18
|
+
* It is worth running anyway because today nothing notices at all: a credential
|
|
19
|
+
* emitted by a third-party tool into ordinary output is indistinguishable from
|
|
20
|
+
* ordinary output until a human happens to look. This converts a silent
|
|
21
|
+
* exposure into a recorded one with a named blast radius, which is what the
|
|
22
|
+
* incident duty needs and currently cannot get.
|
|
23
|
+
*
|
|
24
|
+
* Do not describe it as prevention anywhere — in docs, help text, commit
|
|
25
|
+
* messages or channel posts. A guard advertised as preventing a leak it only
|
|
26
|
+
* reports afterwards retires the worry without retiring the exposure, and that
|
|
27
|
+
* is worse than no guard.
|
|
28
|
+
*
|
|
29
|
+
* It never blocks. Every path answers `{ continue: true }`, including its own
|
|
30
|
+
* failures, following the fail-open idiom the catalog already uses.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readFileSync } from "fs";
|
|
34
|
+
|
|
35
|
+
/** The scanner surface this hook uses: `scanInputExposures` from @hasna/secrets. */
|
|
36
|
+
export type ScanFn = (options: { text?: string; maxBytes?: number; timeoutMs?: number }) => {
|
|
37
|
+
schema: string;
|
|
38
|
+
version: number;
|
|
39
|
+
source: string;
|
|
40
|
+
root: string;
|
|
41
|
+
redacted: true;
|
|
42
|
+
limits: { findings: number; maxFileBytes?: number; timeoutMs?: number };
|
|
43
|
+
stats: {
|
|
44
|
+
filesScanned: number;
|
|
45
|
+
filesSkipped: number;
|
|
46
|
+
bytesScanned: number;
|
|
47
|
+
errors: string[];
|
|
48
|
+
skipped?: { path: string; reason: string; bytes?: number }[];
|
|
49
|
+
};
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
truncatedReason?: string;
|
|
52
|
+
findings: {
|
|
53
|
+
id: string;
|
|
54
|
+
detector: string;
|
|
55
|
+
severity: string;
|
|
56
|
+
path: string;
|
|
57
|
+
line: number;
|
|
58
|
+
column: number;
|
|
59
|
+
/** Already redacted by the scanner — the value never appears here. */
|
|
60
|
+
preview: string;
|
|
61
|
+
evidencePath: string;
|
|
62
|
+
}[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export interface HookInput {
|
|
66
|
+
session_id?: string;
|
|
67
|
+
cwd?: string;
|
|
68
|
+
hook_event_name?: string;
|
|
69
|
+
tool_name?: string;
|
|
70
|
+
tool_input?: Record<string, unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* THE FIELD CLAUDE CODE ACTUALLY SENDS. Verified against the Claude Code
|
|
73
|
+
* 2.1.226 binary on 2026-08-10, in both its embedded documentation and its
|
|
74
|
+
* implementation:
|
|
75
|
+
*
|
|
76
|
+
* "tool_input": { ... },
|
|
77
|
+
* "tool_response": { "success": true } // PostToolUse only
|
|
78
|
+
*
|
|
79
|
+
* hook_event_name:"PostToolUse", tool_name:e, tool_input:r,
|
|
80
|
+
* tool_response:n, tool_use_id:t, duration_ms:l
|
|
81
|
+
*
|
|
82
|
+
* Counted in the same binary: `tool_response` 21 occurrences,
|
|
83
|
+
* `tool_output` 2 — and both of those are the telemetry event name
|
|
84
|
+
* `tengu_dead_probe_hook_updated_mcp_tool_output`, neither adjacent to
|
|
85
|
+
* `PostToolUse`. Control: a deliberately absent string returns 0.
|
|
86
|
+
*
|
|
87
|
+
* Read this before "simplifying" the two fields below into one. Every other
|
|
88
|
+
* hook in this catalog reads `tool_output` only, so on Claude Code they
|
|
89
|
+
* receive `undefined` and silently observe nothing — which for a scanner
|
|
90
|
+
* means a clean verdict over an empty string. That is the vacuous-gate
|
|
91
|
+
* defect this hook exists to notice, and it nearly shipped inside it.
|
|
92
|
+
*/
|
|
93
|
+
tool_response?: Record<string, unknown> | string;
|
|
94
|
+
/** Compatibility only: the pre-existing catalog convention, and some runtimes may use it. */
|
|
95
|
+
tool_output?: Record<string, unknown> | string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface HookOutput {
|
|
99
|
+
continue: true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ScanOutcome {
|
|
103
|
+
/**
|
|
104
|
+
* `unscanned` exists so that "I looked at all of it and it was clean" and "I
|
|
105
|
+
* could not look" do not share an answer. A guard that reports those
|
|
106
|
+
* identically is the defect this hook was built to notice, committed inside
|
|
107
|
+
* the hook itself.
|
|
108
|
+
*/
|
|
109
|
+
status: "clean" | "found" | "unscanned" | "empty" | "error";
|
|
110
|
+
findingCount: number;
|
|
111
|
+
detectors: string[];
|
|
112
|
+
previews: string[];
|
|
113
|
+
bytesScanned?: number;
|
|
114
|
+
reason?: string;
|
|
115
|
+
error?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Ceiling on what we hand the scanner. Above it the outcome is `unscanned`, never `clean`. */
|
|
119
|
+
export const MAX_SCAN_BYTES = 4_000_000;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wall-clock bound on the scan, well under the scanner's own 10s default.
|
|
123
|
+
*
|
|
124
|
+
* This sits on a tool call's critical path, and the Claude installer target
|
|
125
|
+
* writes no `timeout` into settings.json (only the Codewith target does), so
|
|
126
|
+
* the wiring supplies no outer bound. Measured cost is 1.4-7.3ms for 11 KB and
|
|
127
|
+
* 64-90ms for 2.2 MB, so 2s is generous by more than an order of magnitude and
|
|
128
|
+
* still bounds a pathological input. Exceeding it marks the scan truncated,
|
|
129
|
+
* which this hook reports as `unscanned` rather than clean.
|
|
130
|
+
*/
|
|
131
|
+
export const SCAN_TIMEOUT_MS = 2_000;
|
|
132
|
+
|
|
133
|
+
const OUTPUT_FIELDS = ["stdout", "stderr", "output", "content", "text", "error", "result"] as const;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pull the text out of a tool result, whether it arrived as a string or a record.
|
|
137
|
+
*
|
|
138
|
+
* `tool_response` is what Claude Code sends (see HookInput above); `tool_output`
|
|
139
|
+
* is accepted as a fallback so this also works on any runtime using the older
|
|
140
|
+
* catalog convention. Reading only one of them is how a scanner ends up
|
|
141
|
+
* reporting a confident clean over an empty string.
|
|
142
|
+
*/
|
|
143
|
+
export function extractToolOutputText(input: HookInput): string {
|
|
144
|
+
const output = input.tool_response ?? input.tool_output;
|
|
145
|
+
if (output === undefined || output === null) return "";
|
|
146
|
+
if (typeof output === "string") return output;
|
|
147
|
+
if (typeof output !== "object") return String(output);
|
|
148
|
+
|
|
149
|
+
const parts: string[] = [];
|
|
150
|
+
for (const field of OUTPUT_FIELDS) {
|
|
151
|
+
const value = (output as Record<string, unknown>)[field];
|
|
152
|
+
if (typeof value === "string" && value.length > 0) parts.push(value);
|
|
153
|
+
}
|
|
154
|
+
// Nothing recognised but the record is non-empty: scan its serialised form rather
|
|
155
|
+
// than silently skipping an output shape this list does not know about.
|
|
156
|
+
if (parts.length === 0) {
|
|
157
|
+
try {
|
|
158
|
+
const serialised = JSON.stringify(output);
|
|
159
|
+
if (serialised && serialised !== "{}") return serialised;
|
|
160
|
+
} catch {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
return parts.join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scan the text and classify the result.
|
|
170
|
+
*
|
|
171
|
+
* `scan` is injected so the hook's own behaviour is testable without the
|
|
172
|
+
* scanner, and so a missing or broken @hasna/secrets degrades to `error` and a
|
|
173
|
+
* warning rather than taking the session down with it.
|
|
174
|
+
*/
|
|
175
|
+
export function analyzeToolOutput(text: string, scan: ScanFn): ScanOutcome {
|
|
176
|
+
if (!text || text.length === 0) {
|
|
177
|
+
return { status: "empty", findingCount: 0, detectors: [], previews: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let result: ReturnType<ScanFn>;
|
|
181
|
+
try {
|
|
182
|
+
result = scan({ text, maxBytes: MAX_SCAN_BYTES, timeoutMs: SCAN_TIMEOUT_MS });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return {
|
|
185
|
+
status: "error",
|
|
186
|
+
findingCount: 0,
|
|
187
|
+
detectors: [],
|
|
188
|
+
previews: [],
|
|
189
|
+
error: err instanceof Error ? err.message : String(err),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const findings = result?.findings ?? [];
|
|
194
|
+
if (findings.length > 0) {
|
|
195
|
+
return {
|
|
196
|
+
status: "found",
|
|
197
|
+
findingCount: findings.length,
|
|
198
|
+
detectors: [...new Set(findings.map((f) => f.detector))],
|
|
199
|
+
previews: findings.slice(0, 5).map((f) => `${f.evidencePath} ${f.detector}/${f.severity} ${f.preview}`),
|
|
200
|
+
bytesScanned: result.stats?.bytesScanned,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// No findings is only good news if the whole input was actually read.
|
|
205
|
+
const stats = result?.stats;
|
|
206
|
+
const skipped = stats?.skipped?.length ?? 0;
|
|
207
|
+
const errors = stats?.errors?.length ?? 0;
|
|
208
|
+
const bytes = stats?.bytesScanned ?? 0;
|
|
209
|
+
if (result?.truncated || skipped > 0 || errors > 0 || bytes <= 0) {
|
|
210
|
+
const reason =
|
|
211
|
+
result?.truncatedReason ??
|
|
212
|
+
stats?.skipped?.[0]?.reason ??
|
|
213
|
+
stats?.errors?.[0] ??
|
|
214
|
+
(bytes <= 0 ? "zero bytes scanned" : "incomplete scan");
|
|
215
|
+
return { status: "unscanned", findingCount: 0, detectors: [], previews: [], bytesScanned: bytes, reason };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { status: "clean", findingCount: 0, detectors: [], previews: [], bytesScanned: bytes };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The stderr line(s) a human or agent actually reads. Empty string means stay silent. */
|
|
222
|
+
export function formatWarning(outcome: ScanOutcome): string {
|
|
223
|
+
if (outcome.status === "clean" || outcome.status === "empty") return "";
|
|
224
|
+
|
|
225
|
+
if (outcome.status === "error") {
|
|
226
|
+
return `[scanoutput] could not scan this tool output: ${outcome.error ?? "unknown error"} — treat it as UNSCANNED, not clean.`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (outcome.status === "unscanned") {
|
|
230
|
+
return `[scanoutput] UNSCANNED tool output (${outcome.reason ?? "incomplete"}) — no finding here is not evidence of no credential.`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const lines = [
|
|
234
|
+
`[scanoutput] ${outcome.findingCount} credential shape(s) in tool output ` +
|
|
235
|
+
`[${outcome.detectors.join(", ")}] — this output is ALREADY in the transcript; ` +
|
|
236
|
+
`this hook reports it and cannot remove it.`,
|
|
237
|
+
...outcome.previews.map((p) => ` ${p}`),
|
|
238
|
+
` Record it to the incidents channel by NAME and SCOPE only, never the value, then continue.`,
|
|
239
|
+
];
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function buildHookOutput(_outcome: ScanOutcome): HookOutput {
|
|
244
|
+
// Unconditional. A guard that fails closed on its own bug gets uninstalled.
|
|
245
|
+
return { continue: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readStdinJson(): HookInput | null {
|
|
249
|
+
try {
|
|
250
|
+
const raw = readFileSync(0, "utf-8").trim();
|
|
251
|
+
if (!raw) return null;
|
|
252
|
+
return JSON.parse(raw) as HookInput;
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolve the real scanner. Imported lazily and in-process: a CLI spawn costs a
|
|
260
|
+
* fixed ~1.3-1.8s on this fleet regardless of payload, while the in-process call
|
|
261
|
+
* is ~25ms of import plus ~2ms for 11 KB and ~80ms for 2.2 MB (measured
|
|
262
|
+
* 2026-08-10, station01). The spawn is the dominant term and is avoidable.
|
|
263
|
+
*/
|
|
264
|
+
async function loadScanner(): Promise<ScanFn> {
|
|
265
|
+
const mod = (await import("@hasna/secrets/scanner")) as unknown as { scanInputExposures: ScanFn };
|
|
266
|
+
if (typeof mod?.scanInputExposures !== "function") {
|
|
267
|
+
throw new Error("@hasna/secrets/scanner does not export scanInputExposures");
|
|
268
|
+
}
|
|
269
|
+
return mod.scanInputExposures;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function respond(output: HookOutput): void {
|
|
273
|
+
console.log(JSON.stringify(output));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function warn(message: string): void {
|
|
277
|
+
if (message) console.error(message);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function run(): Promise<void> {
|
|
281
|
+
try {
|
|
282
|
+
const input = readStdinJson();
|
|
283
|
+
if (!input) {
|
|
284
|
+
respond({ continue: true });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const text = extractToolOutputText(input);
|
|
289
|
+
if (!text) {
|
|
290
|
+
respond({ continue: true });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let scan: ScanFn;
|
|
295
|
+
try {
|
|
296
|
+
scan = await loadScanner();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
warn(
|
|
299
|
+
`[scanoutput] could not scan this tool output: ${
|
|
300
|
+
err instanceof Error ? err.message : String(err)
|
|
301
|
+
} — treat it as UNSCANNED, not clean.`,
|
|
302
|
+
);
|
|
303
|
+
respond({ continue: true });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const outcome = analyzeToolOutput(text, scan);
|
|
308
|
+
warn(formatWarning(outcome));
|
|
309
|
+
respond(buildHookOutput(outcome));
|
|
310
|
+
} catch (err) {
|
|
311
|
+
// Nothing this hook does may end a session.
|
|
312
|
+
warn(`[scanoutput] hook failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
313
|
+
respond({ continue: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (import.meta.main) {
|
|
318
|
+
void run();
|
|
319
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# workspace-repos-guard
|
|
2
|
+
|
|
3
|
+
Codewith-native hook installed as `hooks run workspace-repos-guard`.
|
|
4
|
+
|
|
5
|
+
PreToolUse guard for the canonical workspace structure (knowledge
|
|
6
|
+
`k_mssu9jdq_dgnnu2`): `$HOME/workspace` contains ONLY `repos/` + `scratch/` +
|
|
7
|
+
`AGENTS.md`, and `repos/` contains ONLY GitHub-org folders. Checkouts at
|
|
8
|
+
`$HOME/workspace/repos/<org>/<repo>/` are read/context only.
|
|
9
|
+
|
|
10
|
+
## What it blocks
|
|
11
|
+
|
|
12
|
+
- Any write to `$HOME/workspace/repos` itself (file tools and Bash).
|
|
13
|
+
- Any write that would create a top-level entry directly under `repos/`
|
|
14
|
+
(depth 1 — a stray folder or file at the org level).
|
|
15
|
+
- Any write whose second path segment is not an allowed GitHub org.
|
|
16
|
+
- Any delete (`rm`, `rmdir`, `git clean`, `git rm`, `unlink`, `shred`,
|
|
17
|
+
`trash`, ...) anywhere under `$HOME/workspace/repos`, at any depth,
|
|
18
|
+
including deep inside org checkouts.
|
|
19
|
+
|
|
20
|
+
## What it allows
|
|
21
|
+
|
|
22
|
+
- Reads, always.
|
|
23
|
+
- Writes deeper inside an allowed org folder
|
|
24
|
+
(`repos/<org>/<repo>/...`). Structure only: it deliberately does NOT
|
|
25
|
+
duplicate the `worktree-guard` hook, which owns edits-in-shared-checkouts
|
|
26
|
+
semantics.
|
|
27
|
+
|
|
28
|
+
Home spellings (`~`, `$HOME`, `${HOME}`, quoted or not, including split-quote
|
|
29
|
+
forms like `"$HOME"/workspace/repos`, which Bash treats identically to the
|
|
30
|
+
unquoted spelling) are expanded before classification; `apply_patch` tools
|
|
31
|
+
are inspected through their `Add File` / `Update File` / `Delete File`
|
|
32
|
+
markers; Bash relative operands are resolved against the command's cwd when
|
|
33
|
+
it sits under `repos/`; parenthesized command groups
|
|
34
|
+
(`(cd ... && rm -rf ...)`) are unwrapped.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Allowed orgs default to `hasna,hasnaxyz,hasna-internal,hasna-products` and
|
|
39
|
+
are overridable with the `WORKSPACE_REPOS_GUARD_ORGS` env var
|
|
40
|
+
(comma-separated). The home directory is resolved with `os.homedir()` —
|
|
41
|
+
never hardcoded.
|
|
42
|
+
|
|
43
|
+
## Failure mode
|
|
44
|
+
|
|
45
|
+
Fail-open: on any parse or evaluation error the hook responds `continue` so a
|
|
46
|
+
guard defect can never wedge the agent.
|
|
47
|
+
|
|
48
|
+
## Known limitation
|
|
49
|
+
|
|
50
|
+
The guard is a best-effort **structural** guard, not an execution sandbox.
|
|
51
|
+
Variable indirection cannot be caught by pre-expansion inspection: a command
|
|
52
|
+
that builds its target dynamically (`R=...; rm -rf $R`, loops over computed
|
|
53
|
+
paths, scripts downloaded and executed at runtime) is undetectable at hook
|
|
54
|
+
time. The hook inspects literal spellings of the protected path (`~/...`,
|
|
55
|
+
`$HOME/...`, `${HOME}/...`, split-quote forms such as `"$HOME"/...`, the
|
|
56
|
+
resolved absolute home) and relative operands resolved from the command's
|
|
57
|
+
cwd, so anything the shell would expand or indirect through a variable is
|
|
58
|
+
outside its reach. Because it also fails open, it must never be relied on as
|
|
59
|
+
the only protection layer.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
Apache-2.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "workspace-repos-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codewith-native Workspace Repos Guard hook for @hasna/hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/hook.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"author": "Hasna",
|
|
11
|
+
"license": "Apache-2.0"
|
|
12
|
+
}
|