oathbound 0.17.0 → 0.17.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.
Files changed (3) hide show
  1. package/README.md +48 -21
  2. package/dist/cli.cjs +25 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,53 +1,80 @@
1
1
  # oathbound
2
2
 
3
- Install and verify Claude Code skills from the [Oath Bound](https://oathbound.ai) registry.
3
+ Install, verify, and publish Claude Code skills and agents from the [Oath Bound](https://oathbound.ai) registry.
4
4
 
5
- Skills are downloaded as tarballs from the registry and verified using SHA-256 content hashing. Every session start and every tool invocation can be checked against the registry to detect tampering.
5
+ Skills and agents are downloaded as tarballs from the registry and verified using SHA-256 content hashing. Every session start and every tool invocation can be checked against the registry to detect tampering.
6
6
 
7
7
  ## Installation
8
8
 
9
- Requires the [Bun](https://bun.sh) runtime.
9
+ ```sh
10
+ npm install -g oathbound
11
+ ```
12
+
13
+ Or via npx (no install required):
10
14
 
11
15
  ```sh
12
- bun add -g oathbound
16
+ npx oathbound <command>
13
17
  ```
14
18
 
15
- Or via npm:
19
+ ## Quick start
16
20
 
17
21
  ```sh
18
- npm install -g oathbound
22
+ oathbound init
19
23
  ```
20
24
 
21
- ## Usage
25
+ The `init` wizard sets up Claude Code hooks (globally and in the current project) so that skills are verified on every session start and tool invocation.
26
+
27
+ ## Commands
22
28
 
23
- ### Install a skill
29
+ ### Skills
24
30
 
25
31
  ```sh
26
- oathbound pull <namespace/skill-name>
27
- oathbound install <namespace/skill-name>
32
+ oathbound pull <namespace/skill[@version]> [--global] # Download & verify a skill
33
+ oathbound push [path] [--private] # Publish a skill to the registry
34
+ oathbound search [query] # Search skills in the registry
35
+ oathbound list # List all public skills
28
36
  ```
29
37
 
30
- Downloads the latest version of a skill from the registry, verifies the tarball hash, and extracts it into your `.claude/skills/` directory.
38
+ `pull` (aliases: `install`, `i`) downloads the latest version of a skill from the registry, verifies the tarball hash, and extracts it into `.claude/skills/`. Pin a specific version with `@1.2.3`. Use `--global` to install into `~/.claude/skills/` instead of the project directory.
39
+
40
+ ### Agents
31
41
 
32
- ### Verify all installed skills (SessionStart hook)
42
+ ```sh
43
+ oathbound agent pull <namespace/name[@version]> # Download an agent
44
+ oathbound agent push [path] [--private] # Publish an agent .md file
45
+ oathbound agent search [query] # Search agents in the registry
46
+ oathbound agent list # List all public agents
47
+ ```
48
+
49
+ ### Verification (hooks)
33
50
 
34
51
  ```sh
35
- oathbound verify
52
+ oathbound verify # SessionStart hook — verify all installed skills
53
+ oathbound verify --check # PreToolUse hook — check skill integrity mid-session
36
54
  ```
37
55
 
38
- Reads session context from stdin, hashes every skill directory under `.claude/skills/`, and compares each hash against the registry. Writes a session state file so subsequent checks are fast. Exits non-zero if any skill fails verification.
56
+ ### Auth
39
57
 
40
- ### Check a skill before tool execution (PreToolUse hook)
58
+ ```sh
59
+ oathbound login # Authenticate with oathbound.ai
60
+ oathbound logout # Clear stored credentials
61
+ oathbound whoami # Show current user
62
+ ```
63
+
64
+ ### Setup
41
65
 
42
66
  ```sh
43
- oathbound verify --check
67
+ oathbound init [--global|--local] # Interactive setup wizard
68
+ oathbound setup # Non-interactive (runs via npm prepare hook)
44
69
  ```
45
70
 
46
- Reads tool invocation context from stdin, re-hashes the relevant skill directory, and compares it against the hash recorded at session start. If the skill was modified after verification, the hook denies execution.
71
+ `init` configures Claude Code hooks in your settings. By default it sets up both global (`~/.claude/settings.json`) and local (`.claude/settings.json`) hooks. Pass `--global` or `--local` to configure only one scope.
72
+
73
+ `setup` is meant to run automatically via the `prepare` script when someone installs your project's dependencies. It reads `.oathbound.jsonc` and merges hooks into `.claude/settings.json` without prompts.
47
74
 
48
75
  ## Hook integration
49
76
 
50
- oathbound is designed to run as [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks). Add it to your `.claude/settings.json`:
77
+ oathbound is designed to run as [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks). The easiest way to set this up is `oathbound init`, which adds the hooks to your `.claude/settings.json`:
51
78
 
52
79
  ```json
53
80
  {
@@ -70,14 +97,14 @@ oathbound is designed to run as [Claude Code hooks](https://docs.anthropic.com/e
70
97
 
71
98
  ## How it works
72
99
 
73
- 1. **Pull**: Downloads a skill tarball from Supabase storage, verifies the SHA-256 hash of the tarball against the registry, and extracts the skill into `.claude/skills/`.
100
+ 1. **Pull**: Downloads a skill tarball from the registry, verifies the SHA-256 hash of the tarball, and extracts the skill into `.claude/skills/`.
74
101
 
75
102
  2. **SessionStart verification**: Walks each subdirectory in `.claude/skills/`, collects all files (excluding `node_modules`, lockfiles, and `.DS_Store`), sorts them by path, hashes each file with SHA-256, then hashes the combined manifest. The resulting content hash is compared against the registry. Verified hashes are written to a temporary session state file.
76
103
 
77
104
  3. **PreToolUse verification**: Re-hashes the skill directory on disk and compares it against the hash saved at session start. If the content has changed since verification, the tool invocation is denied. This detects mid-session tampering.
78
105
 
79
- The content hash algorithm is deterministic: files are sorted lexicographically by relative path, each file is individually hashed, and the concatenated `path\0hash` lines are hashed together. The same algorithm runs on both the registry (frontend) and the CLI to guarantee parity.
106
+ The content hash algorithm is deterministic: files are sorted lexicographically by relative path, each file is individually hashed, and the concatenated `path\0hash` lines are hashed together. The same algorithm runs on both the registry and the CLI to guarantee parity.
80
107
 
81
108
  ## License
82
109
 
83
- Business Source License 1.1 (BUSL-1.1). The Change Date is 2028-03-04, after which the code is available under Apache 2.0.
110
+ MIT
package/dist/cli.cjs CHANGED
@@ -17954,6 +17954,13 @@ ${TEAL}${BOLD}⬡ oathbound${RESET} ${YELLOW}⚠ Warning:${RESET} skill ${BOLD}"
17954
17954
  `);
17955
17955
  process.exit(0);
17956
17956
  }
17957
+ function enforceSkill(skillName, reason, enforcement) {
17958
+ if (enforcement === "warn") {
17959
+ warnSkill(skillName, reason);
17960
+ } else {
17961
+ denySkill(skillName, reason, enforcement);
17962
+ }
17963
+ }
17957
17964
  function isExternalSkillAccess(toolName, toolInput, knownDirs, baseName) {
17958
17965
  const resolvedDirs = knownDirs.map((d) => import_node_path5.resolve(d));
17959
17966
  const isUnderKnownDir = (p) => {
@@ -18212,27 +18219,15 @@ async function verifyCheck() {
18212
18219
  process.exit(0);
18213
18220
  }
18214
18221
  if (!skillDir || !import_node_fs5.existsSync(skillDir) || !import_node_fs5.statSync(skillDir).isDirectory()) {
18215
- if (enforcement === "warn") {
18216
- warnSkill(baseName, "not installed locally");
18217
- } else {
18218
- denySkill(baseName, "not installed locally", enforcement);
18219
- }
18222
+ enforceSkill(baseName, "not installed locally", enforcement);
18220
18223
  }
18221
18224
  const currentHash = hashSkillDir(skillDir);
18222
18225
  const sessionHash = state.verified[baseName];
18223
18226
  if (!sessionHash) {
18224
- if (enforcement === "warn") {
18225
- warnSkill(baseName, "not verified at session start");
18226
- } else {
18227
- denySkill(baseName, "not verified at session start", enforcement);
18228
- }
18227
+ enforceSkill(baseName, "not verified at session start", enforcement);
18229
18228
  }
18230
18229
  if (currentHash !== sessionHash) {
18231
- if (enforcement === "warn") {
18232
- warnSkill(baseName, `modified since session start (${currentHash.slice(0, 8)}… ≠ ${sessionHash.slice(0, 8)}…)`);
18233
- } else {
18234
- denySkill(baseName, `modified since session start — tampering detected (${currentHash.slice(0, 8)}… ≠ ${sessionHash.slice(0, 8)}…)`, enforcement);
18235
- }
18230
+ enforceSkill(baseName, `modified since session start (${currentHash.slice(0, 8)}… ≠ ${sessionHash.slice(0, 8)}…)`, enforcement);
18236
18231
  }
18237
18232
  process.stderr.write(`${GREEN} ${baseName}: ${currentHash} ✓${RESET}
18238
18233
  `);
@@ -18245,9 +18240,13 @@ var import_node_fs6 = require("node:fs");
18245
18240
  var import_node_http = require("node:http");
18246
18241
  var import_node_path6 = require("node:path");
18247
18242
  var import_node_os3 = require("node:os");
18243
+
18244
+ // constants.ts
18248
18245
  var SUPABASE_URL = "https://mjnfqagwuewhgwbtrdgs.supabase.co";
18249
18246
  var SUPABASE_ANON_KEY = "sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n";
18250
18247
  var API_BASE = process.env.OATHBOUND_API_URL ?? "https://www.oathbound.ai";
18248
+
18249
+ // auth.ts
18251
18250
  var AUTH_DIR = import_node_path6.join(import_node_os3.homedir(), ".oathbound");
18252
18251
  var AUTH_FILE = import_node_path6.join(AUTH_DIR, "auth.json");
18253
18252
  function saveSession(session) {
@@ -18383,7 +18382,7 @@ async function getAccessToken() {
18383
18382
  saveSession({
18384
18383
  access_token: data.session.access_token,
18385
18384
  refresh_token: data.session.refresh_token,
18386
- expires_at: data.session.expires_at
18385
+ expires_at: data.session.expires_at ?? Math.floor(Date.now() / 1000) + 3600
18387
18386
  });
18388
18387
  return data.session.access_token;
18389
18388
  }
@@ -18408,7 +18407,6 @@ ${BRAND}`);
18408
18407
  var import_node_fs7 = require("node:fs");
18409
18408
  var import_node_path7 = require("node:path");
18410
18409
  var import_yaml2 = __toESM(require_dist(), 1);
18411
- var API_BASE2 = process.env.OATHBOUND_API_URL ?? "https://www.oathbound.ai";
18412
18410
  async function push(pathArg, options) {
18413
18411
  Wt2(BRAND);
18414
18412
  const skillDir = resolveSkillDir(pathArg);
@@ -18452,7 +18450,7 @@ async function push(pathArg, options) {
18452
18450
  console.log(`${DIM} ${files.length} file(s)${RESET}`);
18453
18451
  const token = await getAccessToken();
18454
18452
  const spin = spinner("Pushing...");
18455
- const response = await fetch(`${API_BASE2}/api/skills`, {
18453
+ const response = await fetch(`${API_BASE}/api/skills`, {
18456
18454
  method: "POST",
18457
18455
  headers: {
18458
18456
  "Content-Type": "application/json",
@@ -18506,7 +18504,6 @@ function parseFrontmatter(content) {
18506
18504
  }
18507
18505
 
18508
18506
  // search.ts
18509
- var API_BASE3 = process.env.OATHBOUND_API_URL ?? "https://www.oathbound.ai";
18510
18507
  function parseSearchArgs(args) {
18511
18508
  const opts = {};
18512
18509
  let i = 0;
@@ -18545,7 +18542,7 @@ async function search(opts) {
18545
18542
  params.set("limit", String(opts.limit));
18546
18543
  if (opts.offset != null)
18547
18544
  params.set("offset", String(opts.offset));
18548
- const url = `${API_BASE3}/api/skills?${params}`;
18545
+ const url = `${API_BASE}/api/skills?${params}`;
18549
18546
  const sp = spinner("Searching...");
18550
18547
  let res;
18551
18548
  try {
@@ -18618,7 +18615,6 @@ ${BRAND} ${TEAL}${showing}${RESET}
18618
18615
  var import_node_fs8 = require("node:fs");
18619
18616
  var import_node_path8 = require("node:path");
18620
18617
  var import_yaml3 = __toESM(require_dist(), 1);
18621
- var API_BASE4 = process.env.OATHBOUND_API_URL ?? "https://www.oathbound.ai";
18622
18618
  function parseAgentFrontmatter(content) {
18623
18619
  const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
18624
18620
  if (!match)
@@ -18720,7 +18716,7 @@ async function agentPush(pathArg, options) {
18720
18716
  visibility
18721
18717
  };
18722
18718
  const spin = spinner("Pushing...");
18723
- const response = await fetch(`${API_BASE4}/api/agents`, {
18719
+ const response = await fetch(`${API_BASE}/api/agents`, {
18724
18720
  method: "POST",
18725
18721
  headers: {
18726
18722
  "Content-Type": "application/json",
@@ -18744,7 +18740,6 @@ async function agentPush(pathArg, options) {
18744
18740
  }
18745
18741
 
18746
18742
  // agent-search.ts
18747
- var API_BASE5 = process.env.OATHBOUND_API_URL ?? "https://www.oathbound.ai";
18748
18743
  function parseAgentSearchArgs(args) {
18749
18744
  const opts = {};
18750
18745
  let i = 0;
@@ -18783,7 +18778,7 @@ async function agentSearch(opts) {
18783
18778
  params.set("limit", String(opts.limit));
18784
18779
  if (opts.offset != null)
18785
18780
  params.set("offset", String(opts.offset));
18786
- const url = `${API_BASE5}/api/agents?${params}`;
18781
+ const url = `${API_BASE}/api/agents?${params}`;
18787
18782
  const sp = spinner("Searching agents...");
18788
18783
  let res;
18789
18784
  try {
@@ -18855,10 +18850,7 @@ ${BRAND} ${TEAL}${showing}${RESET}
18855
18850
  }
18856
18851
  }
18857
18852
  // cli.ts
18858
- var VERSION = "0.17.0";
18859
- var SUPABASE_URL2 = "https://mjnfqagwuewhgwbtrdgs.supabase.co";
18860
- var SUPABASE_ANON_KEY2 = "sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n";
18861
- var API_BASE6 = process.env.OATHBOUND_API_URL ?? "https://www.oathbound.ai";
18853
+ var VERSION = "0.17.1";
18862
18854
  function parseSkillArg(arg) {
18863
18855
  const slash = arg.indexOf("/");
18864
18856
  if (slash < 1 || slash === arg.length - 1)
@@ -19039,7 +19031,7 @@ async function pull(skillArg, options = {}) {
19039
19031
  await ensureInitialized();
19040
19032
  console.log(`
19041
19033
  ${BRAND} ${TEAL}↓ Pulling ${fullName}${version3 ? `@${version3}` : ""}...${RESET}`);
19042
- const supabase = createClient(SUPABASE_URL2, SUPABASE_ANON_KEY2);
19034
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
19043
19035
  let skill;
19044
19036
  if (version3 !== null) {
19045
19037
  const { data, error } = await supabase.from("skills").select("id, name, namespace, version, tar_hash, storage_path").eq("namespace", namespace).eq("name", name).eq("version", version3).single();
@@ -19093,7 +19085,7 @@ ${BRAND} ${TEAL}↓ Pulling ${fullName}${version3 ? `@${version3}` : ""}...${RES
19093
19085
  }
19094
19086
  import_node_fs9.unlinkSync(tarFile);
19095
19087
  try {
19096
- const trackRes = await fetch(`${API_BASE6}/api/downloads`, {
19088
+ const trackRes = await fetch(`${API_BASE}/api/downloads`, {
19097
19089
  method: "POST",
19098
19090
  headers: { "Content-Type": "application/json" },
19099
19091
  body: JSON.stringify({ skill_id: skill.id, version: skill.version })
@@ -19115,7 +19107,7 @@ async function agentPull(agentArg) {
19115
19107
  const fullName = `${namespace}/${name}`;
19116
19108
  console.log(`
19117
19109
  ${BRAND} ${TEAL}↓ Pulling agent ${fullName}${version3 ? `@${version3}` : ""}...${RESET}`);
19118
- const supabase = createClient(SUPABASE_URL2, SUPABASE_ANON_KEY2);
19110
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
19119
19111
  let agent;
19120
19112
  if (version3 !== null) {
19121
19113
  const { data, error } = await supabase.from("agents").select("id, name, namespace, version, content_hash, storage_path, config").eq("namespace", namespace).eq("name", name).eq("version", version3).single();
@@ -19178,7 +19170,7 @@ ${YELLOW}${BOLD} ⚠ This agent defines MCP servers (external connections):${RES
19178
19170
  }
19179
19171
  import_node_fs9.writeFileSync(targetPath, content);
19180
19172
  try {
19181
- const trackRes = await fetch(`${API_BASE6}/api/downloads`, {
19173
+ const trackRes = await fetch(`${API_BASE}/api/downloads`, {
19182
19174
  method: "POST",
19183
19175
  headers: { "Content-Type": "application/json" },
19184
19176
  body: JSON.stringify({ agent_id: agent.id, version: agent.version })
@@ -19243,7 +19235,7 @@ if (require.main == module) {
19243
19235
  setup();
19244
19236
  } else if (subcommand === "verify") {
19245
19237
  const isCheck = args.includes("--check");
19246
- const run = isCheck ? verifyCheck : () => verify(SUPABASE_URL2, SUPABASE_ANON_KEY2, VERSION);
19238
+ const run = isCheck ? verifyCheck : () => verify(SUPABASE_URL, SUPABASE_ANON_KEY, VERSION);
19247
19239
  await run().catch((err) => {
19248
19240
  const msg = err instanceof Error ? err.message : "Unknown error";
19249
19241
  process.stderr.write(`oathbound verify: ${msg}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oathbound",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Install verified Claude Code skills and agents from the Oath Bound registry",
5
5
  "license": "MIT",
6
6
  "author": "Josh Anderson",