oathbound 0.3.1 → 0.4.0

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.
Files changed (2) hide show
  1. package/cli.ts +67 -37
  2. package/package.json +1 -1
package/cli.ts CHANGED
@@ -11,7 +11,7 @@ import { join, relative, dirname } from 'node:path';
11
11
  import { tmpdir, homedir, platform } from 'node:os';
12
12
  import { intro, outro, select, cancel, isCancel } from '@clack/prompts';
13
13
 
14
- const VERSION = '0.3.1';
14
+ const VERSION = '0.4.0';
15
15
 
16
16
  // --- Supabase ---
17
17
  const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
@@ -335,12 +335,18 @@ export function writeOathboundConfig(enforcement: EnforcementLevel): boolean {
335
335
  return true;
336
336
  }
337
337
 
338
+ const SKILL_CHECK = { type: 'command', command: 'oathbound verify --check' };
339
+
338
340
  const OATHBOUND_HOOKS = {
339
341
  SessionStart: [
340
342
  { matcher: '', hooks: [{ type: 'command', command: 'oathbound verify' }] },
341
343
  ],
342
344
  PreToolUse: [
343
- { matcher: 'Skill', hooks: [{ type: 'command', command: 'oathbound verify --check' }] },
345
+ { matcher: 'Skill', hooks: [SKILL_CHECK] },
346
+ { matcher: 'Bash', hooks: [SKILL_CHECK] },
347
+ { matcher: 'Read', hooks: [SKILL_CHECK] },
348
+ { matcher: 'Glob', hooks: [SKILL_CHECK] },
349
+ { matcher: 'Grep', hooks: [SKILL_CHECK] },
344
350
  ],
345
351
  };
346
352
 
@@ -607,6 +613,38 @@ async function verify(): Promise<void> {
607
613
  }
608
614
 
609
615
  // --- Verify --check (PreToolUse hook) ---
616
+
617
+ /** Extract skill name from a file path if it references .claude/skills/<name>/... */
618
+ function skillNameFromPath(filePath: string): string | null {
619
+ const marker = '.claude/skills/';
620
+ const idx = filePath.indexOf(marker);
621
+ if (idx === -1) return null;
622
+ const rest = filePath.slice(idx + marker.length);
623
+ const name = rest.split('/')[0];
624
+ return name || null;
625
+ }
626
+
627
+ /** Extract skill name from a bash command if it references .claude/skills/<name>/... */
628
+ function skillNameFromCommand(command: string): string | null {
629
+ const marker = '.claude/skills/';
630
+ const idx = command.indexOf(marker);
631
+ if (idx === -1) return null;
632
+ const rest = command.slice(idx + marker.length);
633
+ const name = rest.split(/[\/\s'"]/)[0];
634
+ return name || null;
635
+ }
636
+
637
+ function denySkill(reason: string): never {
638
+ console.log(JSON.stringify({
639
+ hookSpecificOutput: {
640
+ hookEventName: 'PreToolUse',
641
+ permissionDecision: 'deny',
642
+ permissionDecisionReason: reason,
643
+ },
644
+ }));
645
+ process.exit(0);
646
+ }
647
+
610
648
  async function verifyCheck(): Promise<void> {
611
649
  let input: Record<string, unknown>;
612
650
  try {
@@ -616,18 +654,34 @@ async function verifyCheck(): Promise<void> {
616
654
  process.exit(1);
617
655
  }
618
656
  const sessionId: string = input.session_id as string;
619
- const skillName: string | undefined = (input.tool_input as Record<string, unknown> | undefined)?.skill as string | undefined;
657
+ const toolName: string = (input.tool_name as string) ?? '';
658
+ const toolInput = (input.tool_input as Record<string, unknown>) ?? {};
620
659
 
621
- if (!sessionId || !skillName) {
622
- // Can't verify — allow through (non-skill invocation or missing context)
623
- process.exit(0);
660
+ if (!sessionId) process.exit(0);
661
+
662
+ // Extract skill name based on which tool triggered the hook
663
+ let baseName: string | null = null;
664
+
665
+ if (toolName === 'Skill') {
666
+ const skill = toolInput.skill as string | undefined;
667
+ if (!skill) process.exit(0);
668
+ baseName = skill.includes(':') ? skill.split(':').pop()! : skill;
669
+ } else if (toolName === 'Bash') {
670
+ baseName = skillNameFromCommand((toolInput.command as string) ?? '');
671
+ } else if (toolName === 'Read') {
672
+ baseName = skillNameFromPath((toolInput.file_path as string) ?? '');
673
+ } else if (toolName === 'Glob' || toolName === 'Grep') {
674
+ baseName = skillNameFromPath((toolInput.path as string) ?? '');
675
+ // Also check pattern/glob fields for skill path references
676
+ if (!baseName) baseName = skillNameFromPath((toolInput.pattern as string) ?? '');
677
+ if (!baseName) baseName = skillNameFromPath((toolInput.glob as string) ?? '');
624
678
  }
625
679
 
680
+ // Not a skill-related operation — allow through
681
+ if (!baseName) process.exit(0);
682
+
626
683
  const stateFile = sessionStatePath(sessionId);
627
- if (!existsSync(stateFile)) {
628
- // No session state — session start hook didn't run or no skills installed
629
- process.exit(0);
630
- }
684
+ if (!existsSync(stateFile)) process.exit(0);
631
685
 
632
686
  let state: SessionState;
633
687
  try {
@@ -637,22 +691,12 @@ async function verifyCheck(): Promise<void> {
637
691
  process.exit(1);
638
692
  }
639
693
 
640
- // Extract just the skill name (strip namespace/ prefix if present)
641
- const baseName = skillName.includes(':') ? skillName.split(':').pop()! : skillName;
642
-
643
694
  // Find the skill directory and re-hash
644
695
  const skillsDir = findSkillsDir();
645
696
  const skillDir = join(skillsDir, baseName);
646
697
 
647
698
  if (!existsSync(skillDir) || !statSync(skillDir).isDirectory()) {
648
- console.log(JSON.stringify({
649
- hookSpecificOutput: {
650
- hookEventName: 'PreToolUse',
651
- permissionDecision: 'deny',
652
- permissionDecisionReason: `Oathbound: skill directory not found for "${baseName}"`,
653
- },
654
- }));
655
- process.exit(0);
699
+ denySkill(`Oathbound: skill directory not found for "${baseName}"`);
656
700
  }
657
701
 
658
702
  const currentHash = hashSkillDir(skillDir);
@@ -660,26 +704,12 @@ async function verifyCheck(): Promise<void> {
660
704
 
661
705
  if (!sessionHash) {
662
706
  process.stderr.write(`${RED} ${baseName}: ${currentHash} (not verified at session start)${RESET}\n`);
663
- console.log(JSON.stringify({
664
- hookSpecificOutput: {
665
- hookEventName: 'PreToolUse',
666
- permissionDecision: 'deny',
667
- permissionDecisionReason: `Oathbound: skill "${baseName}" was not verified at session start`,
668
- },
669
- }));
670
- process.exit(0);
707
+ denySkill(`Oathbound: skill "${baseName}" was not verified at session start`);
671
708
  }
672
709
 
673
710
  if (currentHash !== sessionHash) {
674
711
  process.stderr.write(`${RED} ${baseName}: ${currentHash} ≠ ${sessionHash} (tampered)${RESET}\n`);
675
- console.log(JSON.stringify({
676
- hookSpecificOutput: {
677
- hookEventName: 'PreToolUse',
678
- permissionDecision: 'deny',
679
- permissionDecisionReason: `Oathbound: skill "${baseName}" was modified since session start (tampering detected)`,
680
- },
681
- }));
682
- process.exit(0);
712
+ denySkill(`Oathbound: skill "${baseName}" was modified since session start (tampering detected)`);
683
713
  }
684
714
 
685
715
  process.stderr.write(`${GREEN} ${baseName}: ${currentHash} ✓${RESET}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oathbound",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Install verified Claude Code skills from the Oath Bound registry",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Josh Anderson",