diff --git a/.icons/1claw.svg b/.icons/1claw.svg
new file mode 100644
index 000000000..f6854deae
--- /dev/null
+++ b/.icons/1claw.svg
@@ -0,0 +1,9 @@
+
diff --git a/registry/kmjones1979/.images/avatar.png b/registry/kmjones1979/.images/avatar.png
new file mode 100644
index 000000000..dd7f47e62
Binary files /dev/null and b/registry/kmjones1979/.images/avatar.png differ
diff --git a/registry/kmjones1979/README.md b/registry/kmjones1979/README.md
new file mode 100644
index 000000000..5d0510782
--- /dev/null
+++ b/registry/kmjones1979/README.md
@@ -0,0 +1,11 @@
+---
+display_name: Kevin Jones
+bio: Developer building modules for Coder workspaces
+avatar: ./.images/avatar.png
+github: kmjones1979
+status: community
+---
+
+# Kevin Jones
+
+Developer building modules for Coder workspaces.
diff --git a/registry/kmjones1979/modules/oneclaw/README.md b/registry/kmjones1979/modules/oneclaw/README.md
new file mode 100644
index 000000000..171dff65b
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/README.md
@@ -0,0 +1,61 @@
+---
+display_name: 1Claw
+description: Vault-backed secrets and MCP server wiring for 1Claw in Coder workspaces
+icon: ../../../../.icons/1claw.svg
+verified: false
+tags: [secrets, mcp, ai]
+---
+
+# 1Claw
+
+Give every Coder workspace scoped access to [1Claw](https://1claw.xyz) so AI coding agents can read secrets from an encrypted vault instead of hardcoded credentials. The module supports three provisioning modes — Terraform-native, shell bootstrap, and manual — and merges a `streamable-http` MCP server entry into Cursor and Claude Code config files without overwriting other MCP servers.
+
+Upstream source: [github.com/1clawAI/1claw-coder-workspace-module](https://github.com/1clawAI/1claw-coder-workspace-module).
+
+## Usage
+
+### Terraform-native mode (recommended)
+
+Provisions vault, agent, and access policy at `terraform apply`; cleans up on `terraform destroy`.
+
+```tf
+module "oneclaw" {
+ source = "registry.coder.com/kmjones1979/oneclaw/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ master_api_key = var.oneclaw_key
+}
+```
+
+### Manual mode
+
+Use an existing vault and agent API key from the 1Claw dashboard.
+
+```tf
+module "oneclaw" {
+ source = "registry.coder.com/kmjones1979/oneclaw/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ vault_id = var.oneclaw_vault_id
+ api_token = var.oneclaw_agent_key
+}
+```
+
+### Shell bootstrap mode
+
+Creates vault and agent on the first workspace boot, then caches credentials for subsequent starts.
+
+```tf
+module "oneclaw" {
+ source = "registry.coder.com/kmjones1979/oneclaw/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ human_api_key = var.oneclaw_human_key
+}
+```
+
+> [!NOTE]
+> **Terraform-native mode** runs a `local-exec` provisioner on the machine executing Terraform. It needs network access to the 1Claw API, `curl`, and `python3`.
+
+> [!TIP]
+> Combine this module with other registry modules (e.g. Cursor or Claude Code). The MCP setup script merges into existing `mcp.json` files instead of replacing them.
diff --git a/registry/kmjones1979/modules/oneclaw/main.test.ts b/registry/kmjones1979/modules/oneclaw/main.test.ts
new file mode 100644
index 000000000..89e03d8e8
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/main.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, it } from "bun:test";
+import {
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+ findResourceInstance,
+} from "~test";
+
+describe("oneclaw", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent",
+ });
+
+ it("manual mode sets env vars and mcp script", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ vault_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
+ api_token: "ocv_testtoken",
+ });
+
+ const vaultEnv = findResourceInstance(
+ state,
+ "coder_env",
+ "oneclaw_vault_id",
+ );
+ expect(vaultEnv.name).toBe("ONECLAW_VAULT_ID");
+
+ const apiKeyEnv = findResourceInstance(
+ state,
+ "coder_env",
+ "oneclaw_agent_api_key",
+ );
+ expect(apiKeyEnv.name).toBe("ONECLAW_AGENT_API_KEY");
+
+ const baseUrlEnv = findResourceInstance(
+ state,
+ "coder_env",
+ "oneclaw_base_url",
+ );
+ expect(baseUrlEnv.name).toBe("ONECLAW_BASE_URL");
+ expect(baseUrlEnv.value).toBe("https://api.1claw.xyz");
+
+ const mcpScript = findResourceInstance(
+ state,
+ "coder_script",
+ "oneclaw_mcp_setup",
+ );
+ expect(mcpScript.display_name).toBe("1Claw MCP Setup");
+
+ const bootstrapScripts = state.resources.filter(
+ (r) => r.type === "coder_script" && r.name === "oneclaw_bootstrap",
+ );
+ expect(bootstrapScripts.length).toBe(0);
+
+ const provisions = state.resources.filter(
+ (r) => r.type === "null_resource" && r.name === "oneclaw_provision",
+ );
+ expect(provisions.length).toBe(0);
+ });
+
+ it("bootstrap mode creates bootstrap script", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ human_api_key: "1ck_test_human_key",
+ });
+
+ const bootstrap = findResourceInstance(
+ state,
+ "coder_script",
+ "oneclaw_bootstrap",
+ );
+ expect(bootstrap.display_name).toBe("1Claw Bootstrap");
+
+ const provisions = state.resources.filter(
+ (r) => r.type === "null_resource" && r.name === "oneclaw_provision",
+ );
+ expect(provisions.length).toBe(0);
+ });
+
+ it("custom base_url is reflected in env", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ vault_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
+ api_token: "ocv_testtoken",
+ base_url: "https://api.example.com",
+ });
+
+ const baseUrlEnv = findResourceInstance(
+ state,
+ "coder_env",
+ "oneclaw_base_url",
+ );
+ expect(baseUrlEnv.value).toBe("https://api.example.com");
+ });
+});
diff --git a/registry/kmjones1979/modules/oneclaw/main.tf b/registry/kmjones1979/modules/oneclaw/main.tf
new file mode 100644
index 000000000..3dbabfa98
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/main.tf
@@ -0,0 +1,216 @@
+terraform {
+ required_version = ">= 1.4"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.12"
+ }
+ null = {
+ source = "hashicorp/null"
+ version = ">= 3.0"
+ }
+ }
+}
+
+locals {
+ # Which mode are we in?
+ tf_native_mode = var.master_api_key != ""
+ bootstrap_mode = var.human_api_key != "" && !local.tf_native_mode
+ manual_mode = !local.tf_native_mode && !local.bootstrap_mode
+
+ provision_state_file = "${path.module}/.provision-state.json"
+
+ provision_vault_name = (
+ var.provision_vault_name != "" ? var.provision_vault_name :
+ "coder-${data.coder_workspace.me.name}"
+ )
+ provision_agent_name = (
+ var.provision_agent_name != "" ? var.provision_agent_name :
+ "coder-${data.coder_workspace.me.name}-agent"
+ )
+
+ # Resolve effective vault_id and api_token.
+ # In TF-native mode these come from the provision state file after null_resource runs.
+ effective_vault_id = local.tf_native_mode ? local.provisioned_vault_id : var.vault_id
+ effective_token = local.tf_native_mode ? local.provisioned_token : var.api_token
+
+ # Read provision state (only meaningful after null_resource.oneclaw_provision has run).
+ provision_state = local.tf_native_mode && fileexists(local.provision_state_file) ? jsondecode(file(local.provision_state_file)) : {}
+
+ provisioned_vault_id = lookup(local.provision_state, "vault_id", "")
+ provisioned_token = lookup(local.provision_state, "agent_api_key", "")
+ provisioned_agent_id = lookup(local.provision_state, "agent_id", "")
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+# ===========================================================================
+# Terraform-native provisioning (apply-time create, destroy-time cleanup)
+# ===========================================================================
+
+resource "null_resource" "oneclaw_provision" {
+ count = local.tf_native_mode ? 1 : 0
+
+ # All values needed at destroy time must live in triggers (Terraform restriction).
+ triggers = {
+ workspace_id = data.coder_workspace.me.id
+ workspace_name = data.coder_workspace.me.name
+ vault_name = local.provision_vault_name
+ agent_name = local.provision_agent_name
+ state_file = local.provision_state_file
+ base_url = var.base_url
+ master_api_key = var.master_api_key
+ destroy_vault = tostring(var.auto_destroy_vault)
+ }
+
+ provisioner "local-exec" {
+ interpreter = ["bash", "-c"]
+ command = templatefile("${path.module}/scripts/provision.sh", {
+ BASE_URL = var.base_url
+ MASTER_API_KEY = var.master_api_key
+ WORKSPACE_ID = data.coder_workspace.me.id
+ WORKSPACE_NAME = data.coder_workspace.me.name
+ VAULT_NAME = local.provision_vault_name
+ AGENT_NAME = local.provision_agent_name
+ POLICY_PATH = var.provision_policy_path
+ TOKEN_TTL_SECONDS = tostring(var.token_ttl_hours * 3600)
+ STATE_FILE = local.provision_state_file
+ })
+ }
+
+ provisioner "local-exec" {
+ when = destroy
+ interpreter = ["bash", "-c"]
+ command = <<-EOT
+ set -euo pipefail
+ STATE_FILE="${self.triggers.state_file}"
+ API_URL="${self.triggers.base_url}"
+ MASTER_KEY="${self.triggers.master_api_key}"
+ DESTROY_VAULT="${self.triggers.destroy_vault}"
+
+ if [ ! -f "$STATE_FILE" ]; then
+ echo "[1claw-deprovision] No state file — nothing to clean up"
+ exit 0
+ fi
+
+ VAULT_ID=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['vault_id'])")
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['agent_id'])")
+ echo "[1claw-deprovision] Agent: $AGENT_ID Vault: $VAULT_ID"
+
+ # Authenticate
+ AUTH=$(curl -sf -w "\n%%{http_code}" \
+ -H "Content-Type: application/json" \
+ -d "{\"api_key\": \"$MASTER_KEY\"}" \
+ "$API_URL/v1/auth/api-key-token" 2>&1) || {
+ echo "[1claw-deprovision] WARN: Auth failed — manual cleanup needed"
+ rm -f "$STATE_FILE"; exit 0
+ }
+ AUTH_HTTP=$(echo "$AUTH" | tail -1)
+ AUTH_BODY=$(echo "$AUTH" | sed '$d')
+ if [ "$(echo "$AUTH_HTTP" | head -c1)" != "2" ]; then
+ echo "[1claw-deprovision] WARN: Auth HTTP $AUTH_HTTP — manual cleanup needed"
+ rm -f "$STATE_FILE"; exit 0
+ fi
+ JWT=$(python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])" <<< "$AUTH_BODY")
+
+ # Delete agent
+ echo "[1claw-deprovision] Deleting agent $AGENT_ID..."
+ curl -sf -X DELETE -H "Authorization: Bearer $JWT" "$API_URL/v1/agents/$AGENT_ID" >/dev/null 2>&1 \
+ && echo "[1claw-deprovision] Agent deleted" \
+ || echo "[1claw-deprovision] WARN: Agent delete failed (may already be gone)"
+
+ # Optionally delete vault
+ if [ "$DESTROY_VAULT" = "true" ]; then
+ echo "[1claw-deprovision] Deleting vault $VAULT_ID..."
+ curl -sf -X DELETE -H "Authorization: Bearer $JWT" "$API_URL/v1/vaults/$VAULT_ID" >/dev/null 2>&1 \
+ && echo "[1claw-deprovision] Vault deleted" \
+ || echo "[1claw-deprovision] WARN: Vault delete failed (may have secrets or already be gone)"
+ else
+ echo "[1claw-deprovision] Vault $VAULT_ID retained (set auto_destroy_vault = true to delete)"
+ fi
+
+ rm -f "$STATE_FILE"
+ echo "[1claw-deprovision] Cleanup complete"
+ EOT
+ }
+}
+
+# ===========================================================================
+# Environment variables (injected into the workspace agent)
+# ===========================================================================
+
+resource "coder_env" "oneclaw_vault_id" {
+ count = local.effective_vault_id != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "ONECLAW_VAULT_ID"
+ value = local.effective_vault_id
+}
+
+resource "coder_env" "oneclaw_agent_api_key" {
+ count = local.effective_token != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "ONECLAW_AGENT_API_KEY"
+ value = local.effective_token
+}
+
+resource "coder_env" "oneclaw_agent_id" {
+ count = var.agent_id_1claw != "" || local.provisioned_agent_id != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "ONECLAW_AGENT_ID"
+ value = var.agent_id_1claw != "" ? var.agent_id_1claw : local.provisioned_agent_id
+}
+
+resource "coder_env" "oneclaw_base_url" {
+ agent_id = var.agent_id
+ name = "ONECLAW_BASE_URL"
+ value = var.base_url
+}
+
+# ===========================================================================
+# Shell bootstrap (optional, first-run provisioning inside the workspace)
+# ===========================================================================
+
+resource "coder_script" "oneclaw_bootstrap" {
+ count = local.bootstrap_mode ? 1 : 0
+ agent_id = var.agent_id
+ display_name = "1Claw Bootstrap"
+ icon = var.icon
+ run_on_start = true
+ start_blocks_login = true
+
+ script = templatefile("${path.module}/scripts/bootstrap.sh", {
+ HUMAN_API_KEY = var.human_api_key
+ BASE_URL = var.base_url
+ VAULT_ID = var.vault_id
+ VAULT_NAME = var.bootstrap_vault_name
+ AGENT_NAME = var.bootstrap_agent_name != "" ? var.bootstrap_agent_name : "coder-${data.coder_workspace.me.name}"
+ POLICY_PATH = var.bootstrap_policy_path
+ STATE_DIR = "$HOME/.1claw"
+ })
+}
+
+# ===========================================================================
+# MCP config file injection
+# ===========================================================================
+
+resource "coder_script" "oneclaw_mcp_setup" {
+ agent_id = var.agent_id
+ display_name = "1Claw MCP Setup"
+ icon = var.icon
+ run_on_start = true
+ start_blocks_login = false
+
+ script = templatefile("${path.module}/scripts/setup.sh", {
+ MCP_HOST = var.mcp_host
+ VAULT_ID = local.effective_vault_id
+ API_TOKEN = local.effective_token
+ BOOTSTRAP_MODE = local.bootstrap_mode ? "true" : "false"
+ INSTALL_CURSOR_CONFIG = var.install_cursor_config
+ INSTALL_CLAUDE_CONFIG = var.install_claude_config
+ CURSOR_CONFIG_PATH = var.cursor_config_path
+ CLAUDE_CONFIG_PATH = var.claude_config_path
+ })
+}
diff --git a/registry/kmjones1979/modules/oneclaw/main.tftest.hcl b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl
new file mode 100644
index 000000000..9c8ee927a
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl
@@ -0,0 +1,103 @@
+run "manual_mode" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-manual"
+ vault_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+ api_token = "ocv_testtoken"
+ }
+
+ assert {
+ condition = length(coder_env.oneclaw_vault_id) == 1
+ error_message = "ONECLAW_VAULT_ID should be set in manual mode"
+ }
+
+ assert {
+ condition = length(coder_env.oneclaw_agent_api_key) == 1
+ error_message = "ONECLAW_AGENT_API_KEY should be set in manual mode"
+ }
+
+ assert {
+ condition = length(null_resource.oneclaw_provision) == 0
+ error_message = "No provision resource in manual mode"
+ }
+
+ assert {
+ condition = length(coder_script.oneclaw_bootstrap) == 0
+ error_message = "No bootstrap script in manual mode"
+ }
+}
+
+run "terraform_native_mode" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-tf"
+ master_api_key = "1ck_test_master_key"
+ }
+
+ assert {
+ condition = length(null_resource.oneclaw_provision) == 1
+ error_message = "Terraform-native mode should create the provision null_resource"
+ }
+
+ assert {
+ condition = length(coder_script.oneclaw_bootstrap) == 0
+ error_message = "No bootstrap script in terraform-native mode"
+ }
+}
+
+run "bootstrap_mode" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-bootstrap"
+ human_api_key = "1ck_test_human_key"
+ }
+
+ assert {
+ condition = length(coder_script.oneclaw_bootstrap) == 1
+ error_message = "Bootstrap mode should create the bootstrap script"
+ }
+
+ assert {
+ condition = length(null_resource.oneclaw_provision) == 0
+ error_message = "No provision resource in bootstrap mode"
+ }
+}
+
+run "master_key_takes_precedence_over_human" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-priority"
+ master_api_key = "1ck_master"
+ human_api_key = "1ck_human"
+ }
+
+ assert {
+ condition = length(null_resource.oneclaw_provision) == 1
+ error_message = "master_api_key should win when both keys are set"
+ }
+
+ assert {
+ condition = length(coder_script.oneclaw_bootstrap) == 0
+ error_message = "No bootstrap script when master_api_key is set"
+ }
+}
+
+run "custom_base_url" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-mcp"
+ vault_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+ api_token = "ocv_testtoken"
+ base_url = "https://api.example.com"
+ }
+
+ assert {
+ condition = coder_env.oneclaw_base_url.value == "https://api.example.com"
+ error_message = "ONECLAW_BASE_URL should match base_url"
+ }
+}
diff --git a/registry/kmjones1979/modules/oneclaw/outputs.tf b/registry/kmjones1979/modules/oneclaw/outputs.tf
new file mode 100644
index 000000000..f106b092a
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/outputs.tf
@@ -0,0 +1,33 @@
+output "mcp_config_path" {
+ description = "Primary MCP config file path (Cursor). Use this to reference the config from downstream resources."
+ value = var.cursor_config_path
+}
+
+output "claude_config_path" {
+ description = "Claude Code MCP config file path."
+ value = var.install_claude_config ? var.claude_config_path : ""
+}
+
+output "vault_id" {
+ description = "The 1Claw vault ID configured for this workspace."
+ value = local.effective_vault_id
+ sensitive = true
+}
+
+output "scoped_token" {
+ description = "The agent API key (ocv_) for this workspace. Only populated in Terraform-native mode."
+ value = local.provisioned_token
+ sensitive = true
+}
+
+output "agent_id_1claw" {
+ description = "The 1Claw agent UUID provisioned for this workspace."
+ value = local.provisioned_agent_id != "" ? local.provisioned_agent_id : var.agent_id_1claw
+ sensitive = true
+}
+
+output "provisioning_mode" {
+ description = "Which provisioning mode is active: terraform_native, bootstrap, or manual."
+ value = local.tf_native_mode ? "terraform_native" : (local.bootstrap_mode ? "bootstrap" : "manual")
+ sensitive = true
+}
diff --git a/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh b/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh
new file mode 100644
index 000000000..0faeeabaa
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh
@@ -0,0 +1,151 @@
+#!/bin/bash
+set -euo pipefail
+
+LOG_PREFIX="[1claw-bootstrap]"
+
+log() {
+ echo "$LOG_PREFIX $*"
+}
+
+die() {
+ log "ERROR: $*" >&2
+ exit 1
+}
+
+STATE_DIR=$(eval echo "${STATE_DIR}")
+STATE_FILE="$STATE_DIR/bootstrap.json"
+HUMAN_KEY="${HUMAN_API_KEY}"
+API_URL="${BASE_URL}"
+VAULT="${VAULT_ID}"
+VAULT_NAME_IN="${VAULT_NAME}"
+AGENT_NAME_IN="${AGENT_NAME}"
+POLICY_PATH_IN="${POLICY_PATH}"
+
+# --- Early exit if already bootstrapped ---
+if [ -f "$STATE_FILE" ]; then
+ log "Bootstrap state found at $STATE_FILE — skipping provisioning"
+ exit 0
+fi
+
+if [ -z "$HUMAN_KEY" ]; then
+ die "human_api_key is required for bootstrap mode"
+fi
+
+api_call() {
+ local method="$1"
+ local path="$2"
+ local token="$3"
+ local body="$${4:-}"
+
+ local response
+ response=$(curl -s -w "\n%%{http_code}" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $token" \
+ $${body:+-d "$body"} \
+ -X "$method" "$API_URL$path" 2>&1) || {
+ log "API call failed: $method $path"
+ log "Response: $response"
+ return 1
+ }
+
+ local http_code
+ http_code=$(echo "$response" | tail -1)
+ local body_out
+ body_out=$(echo "$response" | sed '$d')
+
+ if [ "$${http_code:0:1}" != "2" ]; then
+ log "API error: $method $path returned HTTP $http_code"
+ log "Response: $body_out"
+ return 1
+ fi
+
+ echo "$body_out"
+}
+
+json_get() {
+ python3 -c "import json,sys; print(json.load(sys.stdin)$1)"
+}
+
+# --- Step 1: Exchange human API key for JWT ---
+log "Authenticating with 1Claw API..."
+AUTH_RESPONSE=$(curl -s -w "\n%%{http_code}" \
+ -H "Content-Type: application/json" \
+ -d "{\"api_key\": \"$HUMAN_KEY\"}" \
+ "$API_URL/v1/auth/api-key-token" 2>&1) || die "Failed to authenticate with human API key"
+
+AUTH_HTTP=$(echo "$AUTH_RESPONSE" | tail -1)
+AUTH_BODY=$(echo "$AUTH_RESPONSE" | sed '$d')
+
+if [ "$${AUTH_HTTP:0:1}" != "2" ]; then
+ die "Authentication failed (HTTP $AUTH_HTTP): $AUTH_BODY"
+fi
+
+JWT=$(echo "$AUTH_BODY" | json_get "['access_token']")
+log "Authenticated successfully"
+
+# --- Step 2: Resolve or create vault ---
+if [ -n "$VAULT" ]; then
+ log "Using provided vault: $VAULT"
+else
+ log "Creating vault '$VAULT_NAME_IN'..."
+ VAULT_RESPONSE=$(api_call POST "/v1/vaults" "$JWT" \
+ "{\"name\": \"$VAULT_NAME_IN\"}") || {
+ log "Vault creation failed — looking for existing vault named '$VAULT_NAME_IN'"
+ VAULTS_RESPONSE=$(api_call GET "/v1/vaults" "$JWT") || die "Failed to list vaults"
+ VAULT=$(echo "$VAULTS_RESPONSE" | python3 -c "
+import json, sys
+vaults = json.load(sys.stdin).get('vaults', [])
+for v in vaults:
+ if v['name'] == '$VAULT_NAME_IN':
+ print(v['id'])
+ sys.exit(0)
+sys.exit(1)
+") || die "Could not find existing vault named '$VAULT_NAME_IN'"
+ log "Found existing vault: $VAULT"
+ }
+ if [ -z "$VAULT" ]; then
+ VAULT=$(echo "$VAULT_RESPONSE" | json_get "['id']")
+ log "Created vault: $VAULT"
+ fi
+fi
+
+# --- Step 3: Create agent ---
+log "Creating agent '$AGENT_NAME_IN'..."
+AGENT_RESPONSE=$(api_call POST "/v1/agents" "$JWT" \
+ "{\"name\": \"$AGENT_NAME_IN\", \"vault_ids\": [\"$VAULT\"]}") || die "Failed to create agent"
+
+AGENT_ID=$(echo "$AGENT_RESPONSE" | json_get "['agent']['id']")
+AGENT_API_KEY=$(echo "$AGENT_RESPONSE" | json_get "['api_key']")
+
+if [ -z "$AGENT_API_KEY" ] || [ "$AGENT_API_KEY" = "None" ]; then
+ die "Agent created but no API key returned — check auth_method"
+fi
+log "Created agent: $AGENT_ID"
+
+# --- Step 4: Create access policy ---
+log "Creating access policy (path: $POLICY_PATH_IN)..."
+api_call POST "/v1/vaults/$VAULT/policies" "$JWT" \
+ "{\"secret_path_pattern\": \"$POLICY_PATH_IN\", \"principal_type\": \"agent\", \"principal_id\": \"$AGENT_ID\", \"permissions\": [\"read\", \"write\"]}" \
+ > /dev/null || die "Failed to create policy"
+log "Policy created — agent can access $POLICY_PATH_IN"
+
+# --- Step 5: Save state ---
+mkdir -p "$STATE_DIR"
+
+python3 - "$STATE_FILE" "$VAULT" "$AGENT_ID" "$AGENT_API_KEY" << 'PYEOF'
+import json, sys
+state = {
+ "vault_id": sys.argv[2],
+ "agent_id": sys.argv[3],
+ "agent_api_key": sys.argv[4]
+}
+with open(sys.argv[1], "w") as f:
+ json.dump(state, f, indent=2)
+PYEOF
+
+chmod 600 "$STATE_FILE"
+
+log "Bootstrap complete — credentials saved to $STATE_FILE"
+log " Vault ID: $VAULT"
+log " Agent ID: $AGENT_ID"
+log " Agent key: $${AGENT_API_KEY:0:12}..."
diff --git a/registry/kmjones1979/modules/oneclaw/scripts/provision.sh b/registry/kmjones1979/modules/oneclaw/scripts/provision.sh
new file mode 100755
index 000000000..893b7afff
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/scripts/provision.sh
@@ -0,0 +1,151 @@
+#!/bin/bash
+set -euo pipefail
+
+LOG_PREFIX="[1claw-provision]"
+log() { echo "$LOG_PREFIX $*"; }
+die() {
+ log "ERROR: $*" >&2
+ exit 1
+}
+
+API_URL="${BASE_URL}"
+MASTER_KEY="${MASTER_API_KEY}"
+WORKSPACE_ID="${WORKSPACE_ID}"
+WORKSPACE_NAME="${WORKSPACE_NAME}"
+VAULT_NAME="${VAULT_NAME}"
+AGENT_NAME="${AGENT_NAME}"
+POLICY_PATH="${POLICY_PATH}"
+TOKEN_TTL_SECS="${TOKEN_TTL_SECONDS}"
+STATE_FILE="${STATE_FILE}"
+
+[ -n "$MASTER_KEY" ] || die "master_api_key is required"
+
+if [ -f "$STATE_FILE" ]; then
+ log "Provision state already exists at $STATE_FILE — skipping"
+ exit 0
+fi
+
+api_call() {
+ local method="$1" path="$2" token="$3" body="$${4:-}"
+ local response http_code body_out
+
+ response=$(curl -sf -w "\n%%{http_code}" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $token" \
+ $${body:+-d "$body"} \
+ -X "$method" "$API_URL$path" 2>&1) || {
+ log "curl failed: $method $path"
+ return 1
+ }
+
+ http_code=$(echo "$response" | tail -1)
+ body_out=$(echo "$response" | sed '$d')
+
+ if [ "$${http_code:0:1}" != "2" ]; then
+ log "API $method $path => HTTP $http_code"
+ log "Body: $body_out"
+ return 1
+ fi
+ echo "$body_out"
+}
+
+json_get() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }
+
+# --- Step 1: Exchange master key for JWT ---
+log "Authenticating..."
+AUTH=$(curl -sf -w "\n%%{http_code}" \
+ -H "Content-Type: application/json" \
+ -d "{\"api_key\": \"$MASTER_KEY\"}" \
+ "$API_URL/v1/auth/api-key-token" 2>&1) || die "Auth request failed"
+
+AUTH_HTTP=$(echo "$AUTH" | tail -1)
+AUTH_BODY=$(echo "$AUTH" | sed '$d')
+[ "$${AUTH_HTTP:0:1}" = "2" ] || die "Auth failed (HTTP $AUTH_HTTP): $AUTH_BODY"
+
+JWT=$(echo "$AUTH_BODY" | json_get "['access_token']")
+log "Authenticated"
+
+# --- Step 2: Resolve or create vault ---
+log "Creating vault '$VAULT_NAME'..."
+VAULT_ID=""
+VAULT_RESP=$(api_call POST "/v1/vaults" "$JWT" \
+ "{\"name\": \"$VAULT_NAME\", \"description\": \"Auto-provisioned for Coder workspace $WORKSPACE_NAME ($WORKSPACE_ID)\"}") && {
+ VAULT_ID=$(echo "$VAULT_RESP" | json_get "['id']")
+ log "Created vault: $VAULT_ID"
+} || {
+ log "Vault creation failed — searching for existing '$VAULT_NAME'"
+ LIST_RESP=$(api_call GET "/v1/vaults" "$JWT") || die "Cannot list vaults"
+ VAULT_ID=$(echo "$LIST_RESP" | python3 -c "
+import json, sys
+for v in json.load(sys.stdin).get('vaults', []):
+ if v['name'] == '$VAULT_NAME':
+ print(v['id']); sys.exit(0)
+sys.exit(1)
+") || die "No vault named '$VAULT_NAME' found"
+ log "Using existing vault: $VAULT_ID"
+}
+
+# --- Step 3: Create agent scoped to this vault ---
+AGENT_PAYLOAD=$(python3 -c "
+import json, sys
+payload = {
+ 'name': '$AGENT_NAME',
+ 'vault_ids': ['$VAULT_ID'],
+ 'description': 'Coder workspace $WORKSPACE_NAME ($WORKSPACE_ID)'
+}
+ttl = int('$TOKEN_TTL_SECS') if '$TOKEN_TTL_SECS' and '$TOKEN_TTL_SECS' != '0' else None
+if ttl:
+ payload['token_ttl_seconds'] = ttl
+print(json.dumps(payload))
+")
+
+log "Creating agent '$AGENT_NAME' (ttl=$${TOKEN_TTL_SECS}s)..."
+AGENT_RESP=$(api_call POST "/v1/agents" "$JWT" "$AGENT_PAYLOAD") || die "Failed to create agent"
+
+AGENT_ID=$(echo "$AGENT_RESP" | json_get "['agent']['id']")
+AGENT_KEY=$(echo "$AGENT_RESP" | json_get "['api_key']")
+
+[ -n "$AGENT_KEY" ] && [ "$AGENT_KEY" != "None" ] || die "Agent created but no API key returned"
+log "Created agent: $AGENT_ID"
+
+# --- Step 4: Create access policy ---
+log "Creating policy (path: $POLICY_PATH)..."
+api_call POST "/v1/vaults/$VAULT_ID/policies" "$JWT" \
+ "{\"secret_path_pattern\": \"$POLICY_PATH\", \"principal_type\": \"agent\", \"principal_id\": \"$AGENT_ID\", \"permissions\": [\"read\", \"write\"]}" \
+ > /dev/null || die "Failed to create policy"
+log "Policy created"
+
+# --- Step 5: Exchange agent key for a scoped JWT ---
+log "Exchanging agent key for scoped token..."
+TOKEN_RESP=$(curl -sf -w "\n%%{http_code}" \
+ -H "Content-Type: application/json" \
+ -d "{\"agent_id\": \"$AGENT_ID\", \"api_key\": \"$AGENT_KEY\"}" \
+ "$API_URL/v1/auth/agent-token" 2>&1) || die "Token exchange failed"
+
+TOKEN_HTTP=$(echo "$TOKEN_RESP" | tail -1)
+TOKEN_BODY=$(echo "$TOKEN_RESP" | sed '$d')
+[ "$${TOKEN_HTTP:0:1}" = "2" ] || die "Token exchange failed (HTTP $TOKEN_HTTP)"
+
+SCOPED_TOKEN=$(echo "$TOKEN_BODY" | json_get "['access_token']")
+log "Got scoped token"
+
+# --- Step 6: Write state file ---
+mkdir -p "$(dirname "$STATE_FILE")"
+python3 - "$STATE_FILE" "$VAULT_ID" "$AGENT_ID" "$AGENT_KEY" "$SCOPED_TOKEN" "$WORKSPACE_ID" << 'PYEOF'
+import json, sys
+state = {
+ "vault_id": sys.argv[2],
+ "agent_id": sys.argv[3],
+ "agent_api_key": sys.argv[4],
+ "scoped_token": sys.argv[5],
+ "workspace_id": sys.argv[6]
+}
+with open(sys.argv[1], "w") as f:
+ json.dump(state, f, indent=2)
+PYEOF
+chmod 600 "$STATE_FILE"
+
+log "Provision complete"
+log " Vault: $VAULT_ID"
+log " Agent: $AGENT_ID"
+log " Key: $${AGENT_KEY:0:12}..."
diff --git a/registry/kmjones1979/modules/oneclaw/scripts/setup.sh b/registry/kmjones1979/modules/oneclaw/scripts/setup.sh
new file mode 100644
index 000000000..3286531c8
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/scripts/setup.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+set -euo pipefail
+
+LOG_PREFIX="[1claw-mcp]"
+
+log() {
+ echo "$LOG_PREFIX $*"
+}
+
+API_TOKEN="${API_TOKEN}"
+VAULT_ID="${VAULT_ID}"
+
+# In bootstrap mode, API_TOKEN and VAULT_ID are empty at templatefile time.
+# Wait for bootstrap.sh to produce the state file (scripts run concurrently).
+BOOTSTRAP_MODE="${BOOTSTRAP_MODE}"
+STATE_FILE="$HOME/.1claw/bootstrap.json"
+if [ -z "$API_TOKEN" ] && [ "$BOOTSTRAP_MODE" = "true" ]; then
+ WAIT_SECS=0
+ while [ ! -f "$STATE_FILE" ] && [ "$WAIT_SECS" -lt 120 ]; do
+ log "Waiting for bootstrap to complete ($WAIT_SECS/120s)..."
+ sleep 3
+ WAIT_SECS=$((WAIT_SECS + 3))
+ done
+fi
+
+if [ -z "$API_TOKEN" ] && [ -f "$STATE_FILE" ]; then
+ log "Loading credentials from bootstrap state"
+ API_TOKEN=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['agent_api_key'])")
+ VAULT_ID=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['vault_id'])")
+fi
+
+if [ -z "$API_TOKEN" ] || [ -z "$VAULT_ID" ]; then
+ log "WARNING: No API token or vault ID available — skipping MCP config"
+ log "Provide api_token + vault_id, or use human_api_key for bootstrap mode"
+ exit 0
+fi
+
+# Build the MCP config JSON via python3 for safe handling of special characters.
+MCP_CONFIG=$(
+ python3 - "$API_TOKEN" "$VAULT_ID" << 'PYEOF'
+import json, sys
+config = {
+ "mcpServers": {
+ "1claw": {
+ "url": "${MCP_HOST}",
+ "headers": {
+ "Authorization": "Bearer " + sys.argv[1],
+ "X-Vault-ID": sys.argv[2]
+ }
+ }
+ }
+}
+print(json.dumps(config, indent=2))
+PYEOF
+)
+
+# Write MCP_CONFIG to a temp file so the merge script can read it safely.
+MCP_CONFIG_TMP=$(mktemp)
+trap 'rm -f "$MCP_CONFIG_TMP"' EXIT
+echo "$MCP_CONFIG" > "$MCP_CONFIG_TMP"
+
+write_config() {
+ local target_path="$1"
+ local label="$2"
+
+ # Expand $HOME in the path
+ target_path=$(eval echo "$target_path")
+
+ local target_dir
+ target_dir=$(dirname "$target_path")
+
+ if [ ! -d "$target_dir" ]; then
+ log "Creating directory $target_dir for $label config"
+ mkdir -p "$target_dir"
+ fi
+
+ if [ -f "$target_path" ]; then
+ log "Merging 1Claw MCP server into existing $label config at $target_path"
+ if command -v python3 &> /dev/null; then
+ python3 - "$target_path" "$MCP_CONFIG_TMP" << 'PYEOF'
+import json, sys
+
+target_path = sys.argv[1]
+new_config_path = sys.argv[2]
+
+existing = {}
+try:
+ with open(target_path) as f:
+ existing = json.load(f)
+except (json.JSONDecodeError, FileNotFoundError):
+ pass
+
+with open(new_config_path) as f:
+ new_server = json.load(f)
+
+existing.setdefault("mcpServers", {}).update(new_server.get("mcpServers", {}))
+
+with open(target_path, "w") as f:
+ json.dump(existing, f, indent=2)
+PYEOF
+ else
+ log "python3 not found — overwriting $target_path"
+ cat "$MCP_CONFIG_TMP" > "$target_path"
+ fi
+ else
+ log "Writing $label MCP config to $target_path"
+ cat "$MCP_CONFIG_TMP" > "$target_path"
+ fi
+
+ chmod 600 "$target_path"
+ log "$label MCP config ready at $target_path"
+}
+
+# Cursor IDE config
+if [ "${INSTALL_CURSOR_CONFIG}" = "true" ]; then
+ write_config "${CURSOR_CONFIG_PATH}" "Cursor"
+fi
+
+# Claude Code config
+if [ "${INSTALL_CLAUDE_CONFIG}" = "true" ]; then
+ write_config "${CLAUDE_CONFIG_PATH}" "Claude Code"
+fi
+
+log "1Claw MCP setup complete"
diff --git a/registry/kmjones1979/modules/oneclaw/variables.tf b/registry/kmjones1979/modules/oneclaw/variables.tf
new file mode 100644
index 000000000..564b902d7
--- /dev/null
+++ b/registry/kmjones1979/modules/oneclaw/variables.tf
@@ -0,0 +1,153 @@
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "vault_id" {
+ type = string
+ description = "The 1Claw vault ID to scope MCP access to. Optional when using bootstrap mode (human_api_key)."
+ default = ""
+
+ validation {
+ condition = var.vault_id == "" || can(regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", var.vault_id))
+ error_message = "vault_id must be a valid UUID or empty (for bootstrap mode)."
+ }
+}
+
+variable "api_token" {
+ type = string
+ sensitive = true
+ description = "1Claw agent API key (starts with ocv_). Optional when using bootstrap mode (human_api_key)."
+ default = ""
+}
+
+variable "human_api_key" {
+ type = string
+ sensitive = true
+ default = ""
+ description = "One-time human 1ck_ API key for auto-provisioning. On first workspace start, creates a vault, agent, and policy automatically. Credentials are cached in ~/.1claw/bootstrap.json for subsequent starts."
+}
+
+variable "bootstrap_vault_name" {
+ type = string
+ default = "coder-workspace"
+ description = "Name for the auto-created vault (only used when vault_id is not provided and human_api_key is set)."
+}
+
+variable "bootstrap_agent_name" {
+ type = string
+ default = ""
+ description = "Name for the auto-created agent. Defaults to coder-."
+}
+
+variable "bootstrap_policy_path" {
+ type = string
+ default = "**"
+ description = "Secret path pattern for the auto-created policy (glob). Defaults to all secrets."
+}
+
+variable "master_api_key" {
+ type = string
+ sensitive = true
+ default = ""
+ description = "Human 1ck_ API key for Terraform-native provisioning. Creates vault + agent at terraform apply; cleans up at terraform destroy. Credentials are available as outputs immediately — no shell bootstrap needed."
+}
+
+variable "token_ttl_hours" {
+ type = number
+ default = 8
+ description = "TTL in hours for the agent's scoped JWT (Terraform-native mode). Set to 0 for the platform default (1 hour)."
+
+ validation {
+ condition = var.token_ttl_hours >= 0 && var.token_ttl_hours <= 720
+ error_message = "token_ttl_hours must be between 0 and 720 (30 days)."
+ }
+}
+
+variable "auto_destroy_vault" {
+ type = bool
+ default = false
+ description = "Whether to delete the provisioned vault on terraform destroy. When false (default), only the agent is deleted."
+}
+
+variable "provision_vault_name" {
+ type = string
+ default = ""
+ description = "Vault name for Terraform-native provisioning. Defaults to coder-."
+}
+
+variable "provision_agent_name" {
+ type = string
+ default = ""
+ description = "Agent name for Terraform-native provisioning. Defaults to coder--agent."
+}
+
+variable "provision_policy_path" {
+ type = string
+ default = "**"
+ description = "Secret path pattern for the auto-created access policy (Terraform-native mode)."
+}
+
+variable "agent_id_1claw" {
+ type = string
+ description = "Optional 1Claw agent UUID. When omitted, the MCP server resolves the agent from the API key prefix."
+ default = ""
+}
+
+variable "mcp_host" {
+ type = string
+ description = "Base URL of the 1Claw MCP server."
+ default = "https://mcp.1claw.xyz/mcp"
+
+ validation {
+ condition = can(regex("^https?://", var.mcp_host))
+ error_message = "mcp_host must start with http:// or https://."
+ }
+}
+
+variable "base_url" {
+ type = string
+ description = "Base URL of the 1Claw Vault API (used by ONECLAW_BASE_URL env var)."
+ default = "https://api.1claw.xyz"
+
+ validation {
+ condition = can(regex("^https?://", var.base_url))
+ error_message = "base_url must start with http:// or https://."
+ }
+}
+
+variable "install_cursor_config" {
+ type = bool
+ description = "Whether to write MCP config to the Cursor IDE config path."
+ default = true
+}
+
+variable "install_claude_config" {
+ type = bool
+ description = "Whether to write MCP config to the Claude Code config path."
+ default = true
+}
+
+variable "cursor_config_path" {
+ type = string
+ description = "Path where the Cursor MCP config file is written."
+ default = "$HOME/.cursor/mcp.json"
+}
+
+variable "claude_config_path" {
+ type = string
+ description = "Path where the Claude Code MCP config file is written."
+ default = "$HOME/.config/claude/mcp.json"
+}
+
+variable "icon" {
+ type = string
+ description = "Icon to display for the setup script in the Coder UI."
+ default = "/icon/vault.svg"
+}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation."
+ default = null
+}