@hasna/hooks 0.12.2 → 0.12.4
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 +40 -7
- package/bin/hooks-mcp.js +2013 -847
- package/bin/index.js +2508 -1291
- package/bin/native-safety-entry.js +20 -15
- package/bin/native-safety-worker.cjs +20 -0
- package/dist/index.js +2268 -1047
- package/dist/lib/claude-skills-coordination.d.ts +25 -3
- package/dist/lib/codex-safety-check.d.ts +1 -1
- package/dist/lib/codex-safety-trust.d.ts +9 -2
- package/dist/lib/codex-settings.d.ts +6 -3
- package/dist/lib/installer.d.ts +9 -1
- package/dist/lib/native-safety-registration.d.ts +8 -2
- package/dist/native-safety.d.ts +4 -1
- package/dist/native-safety.js +2 -0
- package/hooks/codewith-native-common.ts +3 -3
- package/hooks/hook-signed-link-guard/src/hook.ts +5 -2
- package/hooks/hook-trash-guard/src/hook.ts +7 -4
- package/hooks/hook-workspace-repos-guard/README.md +20 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +422 -10
- package/hooks/native-safety-composite.ts +75 -0
- package/hooks/native-safety-entry.ts +7 -53
- package/hooks/native-safety-worker.ts +19 -0
- package/package.json +5 -4
- package/scripts/build-native-worker.ts +152 -0
|
@@ -1,8 +1,30 @@
|
|
|
1
|
+
/** A definite refusal raised before any Claude settings byte is written.
|
|
2
|
+
* `settings_unreadable` (raised by the installer's writer, also for Gemini
|
|
3
|
+
* settings): the file exists but was not read and parsed as a JSON object. */
|
|
4
|
+
export declare class ClaudeSettingsRefusal extends Error {
|
|
5
|
+
readonly code: "skills_policy_unresolved" | "settings_unreadable";
|
|
6
|
+
constructor(code: "skills_policy_unresolved" | "settings_unreadable", message: string);
|
|
7
|
+
}
|
|
8
|
+
/** Skills is in use for the Claude settings under `home` when at least one holds:
|
|
9
|
+
* 1. the current settings text has a hook handler (`hooks.<event>[].hooks[].command`,
|
|
10
|
+
* or a flat `hooks.<event>[].command`) matching SKILLS_HOOK_COMMAND; or
|
|
11
|
+
* 2. the Skills Claude bridge `<home>/.claude/skills/skills-cli/SKILL.md` exists,
|
|
12
|
+
* as any file type. A lookup error other than ENOENT/ENOTDIR counts as present.
|
|
13
|
+
* Returns the evidence found; an empty list means Skills is genuinely absent. */
|
|
14
|
+
export declare function claudeSkillsUsage(before: string | null, home: string): string[];
|
|
15
|
+
/** Every location the bundled Skills package (@hasna/skills 0.10.28, `getDataRoot`)
|
|
16
|
+
* considers for `agent-policy.json`, in its resolution order, with the outcome
|
|
17
|
+
* at each. Only called after Skills found no policy at the selected location. */
|
|
18
|
+
export declare function skillsPolicyLocations(dataDir?: string): string[];
|
|
1
19
|
/** The owning Skills writer commits Claude settings and discovery together.
|
|
2
|
-
* A missing policy permits ordinary standalone installation
|
|
3
|
-
*
|
|
20
|
+
* A missing policy permits ordinary standalone installation only while Skills is
|
|
21
|
+
* genuinely absent (`claudeSkillsUsage`). When Skills is in use but its policy
|
|
22
|
+
* cannot be resolved, it refuses with `skills_policy_unresolved` and writes
|
|
23
|
+
* nothing. Any other refusal propagates before settings are written; it never
|
|
24
|
+
* selects a fallback writer. `beforeWrite` runs once, immediately before the
|
|
25
|
+
* coordinated write starts.
|
|
4
26
|
*/
|
|
5
27
|
export declare function coordinateClaudeSettings(before: string | null, replacement: string, options: {
|
|
6
28
|
home: string;
|
|
7
29
|
dataDir?: string;
|
|
8
|
-
}): boolean;
|
|
30
|
+
}, beforeWrite?: () => void): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { verifyNativeSafetyCommand } from "../native-safety.js";
|
|
2
2
|
/** Exact `codex --version` outputs whose app-server protocol was measured
|
|
3
3
|
* against the real native binary. Anything else refuses as native_unsupported. */
|
|
4
|
-
export declare const SUPPORTED_CODEX_SAFETY_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.154.0-alpha.6.1", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1", "codex-cli 0.157.0", "codex-cli 0.157.1", "codex-cli 0.158.0", "codex-cli 0.159.0", "codex-cli 0.159.2", "codex-cli 0.159.3"];
|
|
4
|
+
export declare const SUPPORTED_CODEX_SAFETY_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.154.0-alpha.6.1", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1", "codex-cli 0.157.0", "codex-cli 0.157.1", "codex-cli 0.158.0", "codex-cli 0.159.0", "codex-cli 0.159.2", "codex-cli 0.159.3", "codex-cli 0.160.0"];
|
|
5
5
|
export type CodexSafetyCode = "native_unavailable" | "native_unsafe_executable" | "native_executable_required" | "native_unsupported" | "native_failed" | "native_discovery_failed" | "hooks_disabled" | "guard_missing" | "guard_ambiguous" | "guard_definition_invalid" | "guard_disabled" | "guard_untrusted" | "guard_changed" | "guard_failed";
|
|
6
6
|
export declare class CodexSafetyError extends Error {
|
|
7
7
|
readonly code: CodexSafetyCode;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { connectCodexSafety, type CodexSafetyOptions } from "./codex-safety-check.js";
|
|
2
2
|
import { verifyNativeSafetyCommand } from "../native-safety.js";
|
|
3
3
|
export declare class CodexTrustError extends Error {
|
|
4
|
-
readonly code: "input_invalid" | "native_changed" | "native_config_invalid" | "preimage_unsafe" | "trust_outcome_uncertain";
|
|
4
|
+
readonly code: "input_invalid" | "native_changed" | "native_config_invalid" | "preimage_unsafe" | "trust_refused" | "trust_outcome_uncertain";
|
|
5
5
|
readonly operationId?: string | undefined;
|
|
6
|
-
|
|
6
|
+
readonly refusal?: {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
} | undefined;
|
|
10
|
+
constructor(code: "input_invalid" | "native_changed" | "native_config_invalid" | "preimage_unsafe" | "trust_refused" | "trust_outcome_uncertain", operationId?: string | undefined, refusal?: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
} | undefined);
|
|
7
14
|
}
|
|
8
15
|
interface Dependencies {
|
|
9
16
|
connect?: typeof connectCodexSafety;
|
|
@@ -2,6 +2,9 @@ export declare function readCodexSettings(path: string): Record<string, any>;
|
|
|
2
2
|
/** Preserve the exact read version. Never write trust state or config.toml. */
|
|
3
3
|
export declare function capturedCodexSettings(settings: Record<string, any>): string | null;
|
|
4
4
|
export declare function assertSettingsPreimage(raw: string | null, expected?: string): void;
|
|
5
|
-
export declare function writeCodexSettings(path: string, settings: Record<string, any>, expectedSha256?: string): void;
|
|
6
|
-
/** Shared standalone writer. Managed Claude settings still use Skills.
|
|
7
|
-
|
|
5
|
+
export declare function writeCodexSettings(path: string, settings: Record<string, any>, expectedSha256?: string, beforeWrite?: () => void): void;
|
|
6
|
+
/** Shared standalone writer. Managed Claude settings still use Skills.
|
|
7
|
+
* `beforeWrite` runs once, after every predecessor check and immediately before
|
|
8
|
+
* the replacement is renamed over `path`; a failure before it is a definite
|
|
9
|
+
* refusal that left `path` unchanged. */
|
|
10
|
+
export declare function writeHookSettingsPreimage(path: string, settings: Record<string, any>, before: string | null, beforeWrite?: () => void): void;
|
package/dist/lib/installer.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface InstallResult {
|
|
|
28
28
|
applied?: boolean;
|
|
29
29
|
note?: string;
|
|
30
30
|
configPath?: string;
|
|
31
|
+
/** Stable refusal code, when the failure carries one (e.g. skills_policy_unresolved). */
|
|
32
|
+
code?: string;
|
|
31
33
|
}
|
|
32
34
|
export interface InstallOptions {
|
|
33
35
|
/** Optional exact predecessor for a planned native safety registration. */
|
|
@@ -46,6 +48,12 @@ export interface InstallOptions {
|
|
|
46
48
|
codewithMode?: CodewithInstallMode;
|
|
47
49
|
/** Explicit Codewith config path for the direct-write mode and tests. */
|
|
48
50
|
codewithConfigPath?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Single JSON-settings target only: runs once, immediately before the
|
|
53
|
+
* selected writer may change the settings file. A failure reported before it
|
|
54
|
+
* runs is a definite refusal that left the settings unchanged.
|
|
55
|
+
*/
|
|
56
|
+
beforeWrite?: () => void;
|
|
49
57
|
}
|
|
50
58
|
export declare function getSettingsPath(scope?: Scope, target?: SingleTarget, codewithConfigPath?: string): string;
|
|
51
59
|
export declare function getHookPath(name: string): string;
|
|
@@ -80,7 +88,7 @@ export declare function previewNativeReadinessRegistration(target: "codex" | "cl
|
|
|
80
88
|
after: string;
|
|
81
89
|
command: string;
|
|
82
90
|
};
|
|
83
|
-
export declare function installNativeReadinessRegistration(target: "codex" | "claude", expectedSettingsSha256: string): {
|
|
91
|
+
export declare function installNativeReadinessRegistration(target: "codex" | "claude", expectedSettingsSha256: string, beforeWrite?: () => void): {
|
|
84
92
|
success: boolean;
|
|
85
93
|
};
|
|
86
94
|
export declare function installHooks(names: string[], options?: InstallOptions): InstallResult[];
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { verifyNativeSafetyCommand } from "../native-safety.js";
|
|
2
|
+
/** The definite cause of an apply that stopped before its writer could change the target. */
|
|
3
|
+
export interface RegistrationRefusal {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
2
7
|
export declare class NativeRegistrationError extends Error {
|
|
3
|
-
readonly code: "input_invalid" | "settings_unsafe" | "settings_invalid" | "settings_changed" | "hooks_disabled" | "registration_conflict" | "guard_failed" | "apply_outcome_uncertain";
|
|
8
|
+
readonly code: "input_invalid" | "settings_unsafe" | "settings_invalid" | "settings_changed" | "hooks_disabled" | "registration_conflict" | "guard_failed" | "apply_refused" | "apply_outcome_uncertain";
|
|
4
9
|
readonly operationId?: string | undefined;
|
|
5
|
-
|
|
10
|
+
readonly refusal?: RegistrationRefusal | undefined;
|
|
11
|
+
constructor(code: "input_invalid" | "settings_unsafe" | "settings_invalid" | "settings_changed" | "hooks_disabled" | "registration_conflict" | "guard_failed" | "apply_refused" | "apply_outcome_uncertain", operationId?: string | undefined, refusal?: RegistrationRefusal | undefined);
|
|
6
12
|
}
|
|
7
13
|
export interface NativeRegistrationOptions {
|
|
8
14
|
target: "codex" | "claude";
|
package/dist/native-safety.d.ts
CHANGED
|
@@ -55,11 +55,14 @@ export declare function readNativeSafetyRegistration(home?: string): {
|
|
|
55
55
|
export declare function nativeSafetyBindingIntegrityReason(home?: string): string | undefined;
|
|
56
56
|
/** Register only the bundled composite guard. Updates preserve the exact prior
|
|
57
57
|
* record and require its digest. A lock coordinates package-owned writers; an
|
|
58
|
-
* unexpected preimage or a retained interrupted lock requires reconciliation.
|
|
58
|
+
* unexpected preimage or a retained interrupted lock requires reconciliation.
|
|
59
|
+
* `beforeWrite` runs once, after the last predecessor check and immediately
|
|
60
|
+
* before the record is replaced; a failure before it left the record unchanged. */
|
|
59
61
|
export declare function registerNativeSafety(options?: {
|
|
60
62
|
home?: string;
|
|
61
63
|
expectedSHA256?: string;
|
|
62
64
|
command?: string;
|
|
65
|
+
beforeWrite?: () => void;
|
|
63
66
|
}): {
|
|
64
67
|
ok: true;
|
|
65
68
|
changed: boolean;
|
package/dist/native-safety.js
CHANGED
|
@@ -301,8 +301,10 @@ function registerNativeSafety(options = {}) {
|
|
|
301
301
|
if (before) {
|
|
302
302
|
if (hash(readOwned(path)) !== before.sha256)
|
|
303
303
|
throw new NativeSafetyError("binding_changed");
|
|
304
|
+
options.beforeWrite?.();
|
|
304
305
|
renameSync(temporary, path);
|
|
305
306
|
} else {
|
|
307
|
+
options.beforeWrite?.();
|
|
306
308
|
linkSync(temporary, path);
|
|
307
309
|
unlinkSync(temporary);
|
|
308
310
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, writeSync } from "fs";
|
|
2
|
-
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "path";
|
|
3
|
-
import { homedir, tmpdir } from "os";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
|
|
5
5
|
export interface CodewithHookInput {
|
|
6
6
|
session_id?: string;
|
|
@@ -66,7 +66,10 @@ export async function run(): Promise<void> {
|
|
|
66
66
|
if (!("continue" in verdict && verdict.continue === true)) respond(verdict);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Standalone run only. No top-level await: the CommonJS native safety worker
|
|
70
|
+
// (scripts/build-native-worker.ts) bundles this module, and its parser refuses
|
|
71
|
+
// top-level await even where import.meta.main=false makes the block dead.
|
|
72
|
+
// A rejected run still exits non-zero, as an awaited one did.
|
|
69
73
|
if (import.meta.main) {
|
|
70
|
-
|
|
71
|
-
process.exit(0);
|
|
74
|
+
void run().then(() => process.exit(0));
|
|
72
75
|
}
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* command that contains a delete-verb word and stays silent otherwise.
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
-
import { homedir } from "os";
|
|
50
|
-
import { isAbsolute, join, normalize, resolve, sep } from "path";
|
|
49
|
+
import { homedir } from "node:os";
|
|
50
|
+
import { isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
51
51
|
import { BINARY_FAILURES, findTrashBinary, inspectTrashBinary, type TrashBinaryInspection } from "./binary.js";
|
|
52
52
|
export { findTrashBinary, inspectTrashBinary } from "./binary.js";
|
|
53
53
|
import {
|
|
@@ -1297,7 +1297,10 @@ export async function run(): Promise<void> {
|
|
|
1297
1297
|
}
|
|
1298
1298
|
}
|
|
1299
1299
|
|
|
1300
|
+
// Standalone run only. No top-level await: the CommonJS native safety worker
|
|
1301
|
+
// (scripts/build-native-worker.ts) bundles this module, and its parser refuses
|
|
1302
|
+
// top-level await even where import.meta.main=false makes the block dead.
|
|
1303
|
+
// A rejected run still exits non-zero, as an awaited one did.
|
|
1300
1304
|
if (import.meta.main) {
|
|
1301
|
-
|
|
1302
|
-
process.exit(0);
|
|
1305
|
+
void run().then(() => process.exit(0));
|
|
1303
1306
|
}
|
|
@@ -61,6 +61,26 @@ other than `$HOME`, `$( )`, backticks, a glob) fails closed to the directory
|
|
|
61
61
|
the command runs in, so `rm -rf "$TMPDIR/x"` run from a protected checkout is
|
|
62
62
|
refused. Use a literal absolute path for deletes outside the roots.
|
|
63
63
|
|
|
64
|
+
A here-document body is data, not commands, when nothing in the command can
|
|
65
|
+
run it: its delimiter is quoted (or it holds no `$( )` or backticks), and
|
|
66
|
+
every command word of the command, including those in command substitutions
|
|
67
|
+
at any depth and inside double quotes, is a command that never runs its input
|
|
68
|
+
or arguments as code (`cat`, `tee`,
|
|
69
|
+
`head`, `grep`, `jq`, `echo`, `cd`, `mkdir`, `cp`, `mv`, `rm`, `trash`, the
|
|
70
|
+
Hasna record CLIs such as `todos` and `conversations`, `git` with a data
|
|
71
|
+
subcommand such as `commit` and no `-c`, and `gh pr|issue|api|release|gist`).
|
|
72
|
+
No command word may be a path, an expansion, or the bare name of a file the
|
|
73
|
+
command writes, and the command may hold no variable assignment, process
|
|
74
|
+
substitution, or shell, interpreter or runner word (`bash`, `python3`,
|
|
75
|
+
`ssh`, `sudo`, `xargs`, `chmod`, ...). Any other command word (`rbash`,
|
|
76
|
+
`newgrp`, `tclsh8.6`, an unknown tool) keeps the line-by-line analysis,
|
|
77
|
+
because a list of runners can never be complete. A `<<` inside `${...}` or
|
|
78
|
+
`$[...]` is not taken for a here-document. Such a body is analysed on
|
|
79
|
+
its own, away from the session cwd: a protected path it names, or reaches
|
|
80
|
+
with its own `cd`, still counts, but a word in it (`rm`, `>`, a trailing name)
|
|
81
|
+
is never resolved against the cwd, and its `cd` never moves the outer shell.
|
|
82
|
+
Every other body keeps the line-by-line analysis.
|
|
83
|
+
|
|
64
84
|
## Configuration
|
|
65
85
|
|
|
66
86
|
Allowed orgs default to `hasna,hasnaxyz,hasna-products`. Private workspace
|