@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.
@@ -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. Any other refusal
3
- * propagates before settings are written; it never selects a fallback writer.
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
- constructor(code: "input_invalid" | "native_changed" | "native_config_invalid" | "preimage_unsafe" | "trust_outcome_uncertain", operationId?: string | undefined);
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
- export declare function writeHookSettingsPreimage(path: string, settings: Record<string, any>, before: string | null): void;
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;
@@ -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
- constructor(code: "input_invalid" | "settings_unsafe" | "settings_invalid" | "settings_changed" | "hooks_disabled" | "registration_conflict" | "guard_failed" | "apply_outcome_uncertain", operationId?: string | undefined);
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";
@@ -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;
@@ -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
- await run();
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
- await run();
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