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.
- package/cli.ts +67 -37
- 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.
|
|
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: [
|
|
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
|
|
657
|
+
const toolName: string = (input.tool_name as string) ?? '';
|
|
658
|
+
const toolInput = (input.tool_input as Record<string, unknown>) ?? {};
|
|
620
659
|
|
|
621
|
-
if (!sessionId
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`);
|