cc-safety-net 1.0.0 → 1.0.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 CHANGED
@@ -237,6 +237,25 @@ Install CC Safety Net with OpenCode's native plugin command:
237
237
  opencode plugin -g cc-safety-net
238
238
  ```
239
239
 
240
+ > [!NOTE]
241
+ > OpenCode can sometimes keep using a stale cached plugin version. See
242
+ > anomalyco/opencode#25293 for the current tracking issue.
243
+ >
244
+ > To force OpenCode to reinstall `cc-safety-net`, remove its cached package and
245
+ > install the version you want:
246
+ >
247
+ > ```sh
248
+ > rm -rf ~/.cache/opencode/packages/cc-safety-net@latest
249
+ > opencode plugin -g -f cc-safety-net@latest
250
+ >
251
+ > If you prefer pinning a specific version:
252
+ >
253
+ > rm -rf ~/.cache/opencode/packages/cc-safety-net@latest
254
+ > opencode plugin -g -f cc-safety-net@<version>
255
+ >
256
+ > Restart OpenCode after updating so the plugin is loaded from the refreshed
257
+ > cache.
258
+
240
259
  ---
241
260
 
242
261
  ### Pi Installation
@@ -7830,7 +7830,7 @@ import { existsSync as existsSync14 } from "node:fs";
7830
7830
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
7831
7831
  import { tmpdir as tmpdir4 } from "node:os";
7832
7832
  import { delimiter, extname, join as join11 } from "node:path";
7833
- var CURRENT_VERSION = "1.0.0";
7833
+ var CURRENT_VERSION = "1.0.1";
7834
7834
  var VERSION_FETCH_TIMEOUT_MS = 2000;
7835
7835
  var PI_PROBE_TIMEOUT_MS = 5000;
7836
7836
  var PI_SENTINEL_COMMAND = "cc-safety-net";
@@ -9327,7 +9327,7 @@ function formatTraceJson(result) {
9327
9327
  return JSON.stringify(result, null, 2);
9328
9328
  }
9329
9329
  // src/bin/help.ts
9330
- var version = "1.0.0";
9330
+ var version = "1.0.1";
9331
9331
  var INDENT = " ";
9332
9332
  var PROGRAM_NAME = "cc-safety-net";
9333
9333
  function formatOptionFlags(option) {
@@ -11,10 +11,8 @@ type ConfiguredHookAdapter<T> = Omit<HookAdapter<T>, 'outputDeny'> & {
11
11
  createDenyOutput: (message: string) => object;
12
12
  getManualPermissionAdvice?: (reason: string) => boolean | undefined;
13
13
  };
14
- export declare function outputHookDeny(createDenyOutput: (message: string) => object, reason: string, command?: string, segment?: string, manualPermissionAdvice?: boolean): void;
15
- export declare function readHookInput<T>(outputDeny: (reason: string) => void): Promise<T | null>;
16
14
  export declare function parseHookJson<T>(inputText: string, outputDeny: (reason: string) => void, strictReason: string): T | null;
15
+ /** @internal - exported for direct test coverage */
17
16
  export declare function handleBlockedHookCommand(command: string, cwd: string, sessionId: string | undefined, outputDeny: (reason: string, command?: string, segment?: string) => void): void;
18
- export declare function runHookAdapter<T>(adapter: HookAdapter<T>): Promise<void>;
19
17
  export declare function runConfiguredHookAdapter<T>(adapter: ConfiguredHookAdapter<T>): Promise<void>;
20
18
  export {};
@@ -1,10 +1,4 @@
1
1
  import { type LoadedRulesPolicy, type RulebookLockEntryWithStats } from '@/core/rules/policy';
2
- export declare function printSyncResult(result: {
3
- ok: boolean;
4
- errors: string[];
5
- warnings?: string[];
6
- entries: RulebookLockEntryWithStats[];
7
- }): void;
8
2
  export declare function printRuleChangeResult(result: {
9
3
  ok: boolean;
10
4
  errors: string[];
@@ -18,4 +12,3 @@ export declare function printRulesTestResult(result: {
18
12
  entries: RulebookLockEntryWithStats[];
19
13
  }, sourceDisplayMap?: Map<string, string>): void;
20
14
  export declare function printRulesListReport(policy: LoadedRulesPolicy, sourceDisplayMaps: Record<'user' | 'project', Map<string, string>>): void;
21
- export declare function relativeDisplay(cwd: string, path: string): string;
@@ -9,4 +9,5 @@ export interface ParallelAnalyzeContext {
9
9
  analyzeNested: (command: string, overrides?: AnalyzeNestedOverrides) => string | null;
10
10
  }
11
11
  export declare function analyzeParallel(tokens: readonly string[], context: ParallelAnalyzeContext): string | null;
12
+ /** @internal - exported for test coverage */
12
13
  export declare function extractParallelChildCommand(tokens: readonly string[]): string[];
@@ -11,5 +11,6 @@ interface XargsParseResult {
11
11
  childTokens: string[];
12
12
  replacementToken: string | null;
13
13
  }
14
+ /** @internal - exported for test coverage */
14
15
  export declare function extractXargsChildCommandWithInfo(tokens: readonly string[]): XargsParseResult;
15
16
  export {};
@@ -1,6 +1,9 @@
1
1
  export declare const GIT_CONTEXT_ENV_OVERRIDES: readonly ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"];
2
+ /** @internal - exported for test coverage */
2
3
  export declare const GIT_CONFIG_AFFECTING_ENV_NAMES: ReadonlySet<string>;
4
+ /** @internal - exported for test coverage */
3
5
  export declare const GIT_SSH_ENV_NAMES: ReadonlySet<string>;
6
+ /** @internal - exported for test coverage */
4
7
  export declare function isGitContextEnvOverrideName(name: string): boolean;
5
8
  export declare function isGitConfigEnvName(name: string): boolean;
6
9
  export declare function isTrackedGitEnvName(name: string): boolean;
@@ -1,6 +1,5 @@
1
- export { readRulesConfig, validateRulesConfig, writeDefaultRulesConfig, writeStarterRulebook, } from './config-file';
2
- export { getLegacyUserRulesConfigPath, getProjectRulesConfigPath, getProjectRulesDir, getProjectRulesLockPath, getRulebookCachePath, getRulebookDisplaySource, getRulesLockPathForConfigPath, getUserRulesConfigPath, getUserRulesDir, getUserRulesLockPath, RULES_DIR, } from './paths';
3
- export { getRulebookMigratedFrom, getRulesConfigRuntimeErrorsForConfig, getRulesConfigSourceDisplayMap, getUnknownOverrideErrorsForConfig, loadRulesPolicy, rulesPolicyToConfig, } from './scope-policy';
4
- export { parseGitHubSource } from './sources';
5
- export { addRulebookSource, removeRulebookSource, repairLocalRulesPolicy, syncRulesConfig, testRulebookSources, } from './sync';
6
- export type { LoadedRulebookInfo, LoadedRulesPolicy, RulebookLockEntry, RulebookLockEntryWithStats, RulebookSourceKind, RuleOverride, RulesConfig, RulesLockfile, RulesPolicyOptions, SyncRulesConfigOptions, SyncRulesConfigResult, } from './types';
1
+ export { readRulesConfig, writeDefaultRulesConfig, writeStarterRulebook, } from './config-file';
2
+ export { getLegacyUserRulesConfigPath, getProjectRulesConfigPath, getProjectRulesDir, getRulebookDisplaySource, getRulesLockPathForConfigPath, getUserRulesConfigPath, getUserRulesDir, getUserRulesLockPath, RULES_DIR, } from './paths';
3
+ export { getRulebookMigratedFrom, getRulesConfigRuntimeErrorsForConfig, getRulesConfigSourceDisplayMap, loadRulesPolicy, } from './scope-policy';
4
+ export { addRulebookSource, removeRulebookSource, syncRulesConfig, testRulebookSources, } from './sync';
5
+ export type { LoadedRulesPolicy, RulebookLockEntryWithStats, RuleOverride, SyncRulesConfigOptions, } from './types';
@@ -17,6 +17,7 @@ export interface ScopePaths {
17
17
  }
18
18
  export declare function getProjectRulesDir(cwd?: string): string;
19
19
  export declare function getProjectRulesConfigPath(cwd?: string): string;
20
+ /** @internal - exported for test coverage */
20
21
  export declare function getProjectRulesLockPath(cwd?: string): string;
21
22
  export declare function getUserRulesDir(options?: RulesPolicyOptions): string;
22
23
  export declare function getUserRulesConfigPath(options?: RulesPolicyOptions): string;
@@ -11,6 +11,7 @@ interface ScopePolicy {
11
11
  export declare function loadRulesPolicy(options?: RulesPolicyOptions): LoadedRulesPolicy;
12
12
  export declare function getRulesConfigSourceDisplayMap(configPath: string): Map<string, string>;
13
13
  export declare function getRulesConfigRuntimeErrorsForConfig(configPath: string, lockPath: string, options: RulesPolicyOptions): string[];
14
+ /** @internal - exported for test coverage */
14
15
  export declare function getUnknownOverrideErrorsForConfig(configPath: string, lockPath: string, options: RulesPolicyOptions): string[];
15
16
  export declare function loadScopePolicy(config: RulesConfig, lockPath: string, configDir: string, options: RulesPolicyOptions, source: 'user' | 'project'): ScopePolicy;
16
17
  export declare function rulesPolicyToConfig(policy: LoadedRulesPolicy): Config;
@@ -2,7 +2,6 @@ import type { CustomRule } from '@/types';
2
2
  export type RuleOverride = 'off' | {
3
3
  reason: string;
4
4
  };
5
- export type RulebookSourceKind = RulebookLockEntry['kind'];
6
5
  export interface RulesConfig {
7
6
  version: 1;
8
7
  rules: string[];
@@ -14,6 +14,7 @@ export interface Rulebook {
14
14
  rules: CustomRule[];
15
15
  tests: RulebookFixture[];
16
16
  }
17
+ /** @internal - exported for test coverage */
17
18
  export interface RulebookFixtureFailure {
18
19
  command: string;
19
20
  message: string;
@@ -23,6 +24,7 @@ export interface RulebookFixtureResult {
23
24
  ok: boolean;
24
25
  failures: RulebookFixtureFailure[];
25
26
  }
27
+ /** @internal - exported for test coverage */
26
28
  export declare function validateRulebook(rulebook: unknown): ValidationResult;
27
29
  export declare function runRulebookFixtures(rulebook: Rulebook): RulebookFixtureResult;
28
30
  export declare function assertValidRulebook(rulebook: unknown): Rulebook;
@@ -11,5 +11,6 @@ type PiCommandContext = {
11
11
  isIdle: () => boolean;
12
12
  };
13
13
  export declare function registerBuiltinCommands(pi: PiCommandApi): void;
14
+ /** @internal - exported for test coverage */
14
15
  export declare function buildSafetyNetCommandPrompt(args: string): string;
15
16
  export {};
@@ -1,5 +1,5 @@
1
1
  import { registerBuiltinCommands } from '@/pi/builtin-commands';
2
- import { registerToolUseEvent } from '@/pi/tool-use';
3
- type PiExtensionApi = Parameters<typeof registerBuiltinCommands>[0] & Parameters<typeof registerToolUseEvent>[0];
2
+ import { registerToolCallEvent } from '@/pi/tool-call';
3
+ type PiExtensionApi = Parameters<typeof registerBuiltinCommands>[0] & Parameters<typeof registerToolCallEvent>[0];
4
4
  export default function ccSafetyNetPiExtension(pi: PiExtensionApi): void;
5
5
  export {};
package/dist/pi/index.js CHANGED
@@ -275,6 +275,9 @@ function buildSafetyNetCommandPrompt(args) {
275
275
 
276
276
  ${args.trim() || DEFAULT_USER_REQUEST}`;
277
277
  }
278
+ // src/pi/tool-call.ts
279
+ import { resolve as resolve8 } from "node:path";
280
+
278
281
  // src/core/analyze/dangerous-text.ts
279
282
  function dangerousInText(text) {
280
283
  const t = text.toLowerCase();
@@ -6331,22 +6334,34 @@ async function runConfiguredHookAdapter(adapter) {
6331
6334
  });
6332
6335
  }
6333
6336
 
6334
- // src/pi/tool-use.ts
6335
- function registerToolUseEvent(pi) {
6336
- pi.on("tool_call", handlePiToolUse);
6337
+ // src/pi/tool-call.ts
6338
+ var PI_SHELL_TOOL_ADAPTERS = {
6339
+ bash: {
6340
+ commandField: "command"
6341
+ },
6342
+ Shell: {
6343
+ commandField: "command",
6344
+ cwdField: "working_directory"
6345
+ }
6346
+ };
6347
+ function registerToolCallEvent(pi) {
6348
+ pi.on("tool_call", handlePiToolCall);
6337
6349
  }
6338
- function handlePiToolUse(event, ctx) {
6339
- if (!isPiBashToolUseEvent(event))
6350
+ function handlePiToolCall(event, ctx) {
6351
+ const shellToolCall = getPiShellToolCall(event, ctx);
6352
+ if (!shellToolCall)
6340
6353
  return;
6341
- if (typeof event.input.command !== "string") {
6342
- return blockPiToolUse(REASON_SAFETY_NET_FAILED_CLOSED);
6354
+ if ("malformed" in shellToolCall) {
6355
+ return blockPiToolCall(REASON_SAFETY_NET_FAILED_CLOSED);
6343
6356
  }
6357
+ const command2 = shellToolCall.command;
6358
+ const cwd = shellToolCall.cwd;
6344
6359
  const modes = getCCSafetyNetEnvModes();
6345
6360
  let result;
6346
6361
  try {
6347
- result = (ctx.safetyNetAnalyzeCommand ?? analyzeCommand)(event.input.command, {
6348
- cwd: ctx.cwd,
6349
- config: loadConfig(ctx.cwd, {
6362
+ result = (ctx.safetyNetAnalyzeCommand ?? analyzeCommand)(command2, {
6363
+ cwd,
6364
+ config: loadConfig(cwd, {
6350
6365
  repairLocalRulebooks: true,
6351
6366
  ...ctx.safetyNetConfigOptions
6352
6367
  }),
@@ -6357,14 +6372,14 @@ function handlePiToolUse(event, ctx) {
6357
6372
  });
6358
6373
  } catch (error) {
6359
6374
  if (envTruthy(ENV_FLAGS.debug)) {
6360
- console.error(`CC Safety Net debug: pi tool_use analysis failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`);
6375
+ console.error(`CC Safety Net debug: pi tool_call analysis failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`);
6361
6376
  }
6362
- return blockPiToolUse(REASON_SAFETY_NET_FAILED_CLOSED, event.input.command, event.input.command);
6377
+ return blockPiToolCall(REASON_SAFETY_NET_FAILED_CLOSED, command2, command2);
6363
6378
  }
6364
6379
  if (!result) {
6365
6380
  const sessionId2 = ctx.sessionManager.getSessionFile();
6366
6381
  if (sessionId2 && envTruthy(ENV_FLAGS.debug)) {
6367
- writeAuditLog(sessionId2, event.input.command, event.input.command, "allowed", ctx.cwd, {
6382
+ writeAuditLog(sessionId2, command2, command2, "allowed", cwd, {
6368
6383
  decision: "allow"
6369
6384
  });
6370
6385
  }
@@ -6372,17 +6387,29 @@ function handlePiToolUse(event, ctx) {
6372
6387
  }
6373
6388
  const sessionId = ctx.sessionManager.getSessionFile();
6374
6389
  if (sessionId) {
6375
- writeAuditLog(sessionId, event.input.command, result.segment, result.reason, ctx.cwd);
6390
+ writeAuditLog(sessionId, command2, result.segment, result.reason, cwd);
6376
6391
  }
6377
- return blockPiToolUse(result.reason, event.input.command, result.segment, result.manualPermissionAdvice);
6392
+ return blockPiToolCall(result.reason, command2, result.segment, result.manualPermissionAdvice);
6378
6393
  }
6379
- function isPiBashToolUseEvent(event) {
6394
+ function getPiShellToolCall(event, ctx) {
6380
6395
  if (!event || typeof event !== "object")
6381
- return false;
6382
- const toolUse = event;
6383
- return toolUse.toolName === "bash" && !!toolUse.input;
6384
- }
6385
- function blockPiToolUse(reason, command2, segment, manualPermissionAdvice) {
6396
+ return;
6397
+ const toolCall = event;
6398
+ if (typeof toolCall.toolName !== "string")
6399
+ return;
6400
+ const adapter = PI_SHELL_TOOL_ADAPTERS[toolCall.toolName];
6401
+ if (!adapter)
6402
+ return;
6403
+ if (!toolCall.input || typeof toolCall.input !== "object")
6404
+ return { malformed: true };
6405
+ const command2 = toolCall.input[adapter.commandField];
6406
+ if (typeof command2 !== "string")
6407
+ return { malformed: true };
6408
+ const cwdInput = adapter.cwdField ? toolCall.input[adapter.cwdField] : undefined;
6409
+ const cwd = typeof cwdInput === "string" ? resolve8(ctx.cwd, cwdInput) : ctx.cwd;
6410
+ return { command: command2, cwd };
6411
+ }
6412
+ function blockPiToolCall(reason, command2, segment, manualPermissionAdvice) {
6386
6413
  return {
6387
6414
  block: true,
6388
6415
  reason: formatBlockedMessage({
@@ -6397,7 +6424,7 @@ function blockPiToolUse(reason, command2, segment, manualPermissionAdvice) {
6397
6424
 
6398
6425
  // src/pi/index.ts
6399
6426
  function ccSafetyNetPiExtension(pi) {
6400
- registerToolUseEvent(pi);
6427
+ registerToolCallEvent(pi);
6401
6428
  registerBuiltinCommands(pi);
6402
6429
  }
6403
6430
  export {
@@ -1,9 +1,9 @@
1
1
  import { analyzeCommand } from '@/core/analyze';
2
2
  import type { LoadConfigOptions } from '@/core/config';
3
3
  type PiApi = {
4
- on: (event: 'tool_call', handler: (event: unknown, ctx: PiToolUseContext) => PiToolUseResult) => void;
4
+ on: (event: 'tool_call', handler: (event: unknown, ctx: PiToolCallContext) => PiToolCallResult) => void;
5
5
  };
6
- type PiToolUseContext = {
6
+ type PiToolCallContext = {
7
7
  cwd: string;
8
8
  sessionManager: {
9
9
  getSessionFile: () => string | undefined;
@@ -11,10 +11,11 @@ type PiToolUseContext = {
11
11
  safetyNetAnalyzeCommand?: typeof analyzeCommand;
12
12
  safetyNetConfigOptions?: LoadConfigOptions;
13
13
  };
14
- type PiToolUseResult = {
14
+ type PiToolCallResult = {
15
15
  block: true;
16
16
  reason: string;
17
17
  } | undefined;
18
- export declare function registerToolUseEvent(pi: PiApi): void;
19
- export declare function handlePiToolUse(event: unknown, ctx: PiToolUseContext): PiToolUseResult;
18
+ export declare function registerToolCallEvent(pi: PiApi): void;
19
+ /** @internal - exported for test coverage */
20
+ export declare function handlePiToolCall(event: unknown, ctx: PiToolCallContext): PiToolCallResult;
20
21
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safety-net",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A coding agent CLI hook - block destructive git and filesystem commands before execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",