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.
- package/README.md +48 -21
- package/dist/cli.cjs +25 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,80 @@
|
|
|
1
1
|
# oathbound
|
|
2
2
|
|
|
3
|
-
Install and
|
|
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
|
-
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g oathbound
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or via npx (no install required):
|
|
10
14
|
|
|
11
15
|
```sh
|
|
12
|
-
|
|
16
|
+
npx oathbound <command>
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
## Quick start
|
|
16
20
|
|
|
17
21
|
```sh
|
|
18
|
-
|
|
22
|
+
oathbound init
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
|
|
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
|
-
###
|
|
29
|
+
### Skills
|
|
24
30
|
|
|
25
31
|
```sh
|
|
26
|
-
oathbound pull <namespace/skill
|
|
27
|
-
oathbound
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
### Auth
|
|
39
57
|
|
|
40
|
-
|
|
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
|
|
67
|
+
oathbound init [--global|--local] # Interactive setup wizard
|
|
68
|
+
oathbound setup # Non-interactive (runs via npm prepare hook)
|
|
44
69
|
```
|
|
45
70
|
|
|
46
|
-
|
|
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).
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`${
|
|
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 = `${
|
|
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(`${
|
|
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 = `${
|
|
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.
|
|
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(
|
|
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(`${
|
|
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(
|
|
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(`${
|
|
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(
|
|
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}
|