Skip to main content
The credential vault lets a sandbox use a secret (API key, token, database password) without the secret value ever entering the VM. The value lives server-side in OpenBao and is injected at the egress proxy on outbound requests to the destinations you scope it to. Inside the VM, the env var holds only the placeholder string declaw:vault-managed. This is the stronger sibling of Environment Secrets. With env secrets, the value is present in the VM (just masked in audit logs and excluded from get_info()) — any process inside the sandbox can read it from its environment. With the vault, the value is never delivered to the VM at all: a compromised agent that dumps /proc, reads the environment, or exfiltrates the process table sees only the placeholder. The credential is resolved and applied at the proxy layer between the VM and the upstream service. Secrets are stored and referenced by name — there is no team or environment to set up. The value is written to the vault backend (OpenBao) and is never returned by any API after creation.

Using a vault secret in a sandbox

Pass a map of env var name → secret name at sandbox create time.
from declaw import Sandbox

sbx = Sandbox.create(
    template="base",
    network={"allow_out": ["postman-echo.com"]},
    vault_refs={
        "DEMO_TOKEN": "demo-token",
    },
)
import { Sandbox } from "@declaw/sdk";

const sbx = await Sandbox.create({
  network: { allowOut: ["postman-echo.com"] },
  vaultRefs: {
    DEMO_TOKEN: "demo-token",
  },
});
sbx, err := declaw.Create(ctx,
    declaw.WithTemplate("base"),
    declaw.WithNetwork(declaw.SandboxNetworkOpts{
        AllowOut: []string{"postman-echo.com"},
    }),
    declaw.WithVaultRefs(map[string]string{
        "DEMO_TOKEN": "demo-token",
    }),
)
declaw sandbox create \
  --template base \
  --vault-ref DEMO_TOKEN=demo-token
A vault ref resolves by secret namedemo-token looks up the secret named demo-token. The --vault-ref CLI flag is repeatable.
Two requirements for injection to actually happen. A vault ref always hides the value (the VM gets the placeholder), but injection only fires when both of these hold:
  1. The scope’s domain_regex must use the ~ regex prefix. An unprefixed entry is an exact hostname match, so a regex literal like ^postman-echo\.com$ would never equal the real host and nothing would be injected. Write ~^postman-echo\.com$.
  2. The target host must be in the sandbox’s network allow_out. Injection happens at the L7 egress proxy. A host that is not in the network policy bypasses the proxy entirely, so there is nothing to inject into.
Miss either one and the value stays hidden but no credential is attached to the outbound request.

What the agent sees

The env var inside the VM holds the placeholder — never the real value:
$ printenv DEMO_TOKEN
declaw:vault-managed
An outbound request to a scoped host carries the injected credential (here, bearer injection sets the Authorization header). The agent issues a plain request; the proxy attaches the header before forwarding:
$ curl -s https://postman-echo.com/get
# reflected request headers include:
#   "authorization": "Bearer <the-real-value>"
A request to a host not covered by a scope carries nothing — no credential is leaked to unintended destinations:
$ curl -s https://some-other-host.example/get
# no Authorization header — the secret is scoped to postman-echo.com only
The credential never existed anywhere inside the VM. It was fetched worker-side from OpenBao and applied at the proxy. Rotating the secret value takes effect on the next outbound request — no sandbox restart required.

Storing a secret

A secret stores a value plus one or more scopes that tell the egress proxy which destination host the value is for and how to inject it. There are two ways to supply the scopes. The value is write-only: it is written to OpenBao and is never returned by any API after creation — not by create, list, or get.
The vault is a cloud-mode feature and requires an authenticated API key. All operations are scoped to your account.

(a) From a provider preset

Name a built-in provider with provider= and the preset supplies the domain regex, injection type, and any required static headers — you only paste the value. The secret name defaults to the provider key when omitted. Browse the catalog with list_presets / declaw vault presets.
from declaw import VaultClient

vault = VaultClient()  # uses DECLAW_API_KEY

secret = vault.create_secret(
    name="openai",
    value="sk-...",         # never returned
    provider="openai",      # supplies scopes
)
print(secret.secret_id)
import { Vault } from "@declaw/sdk";

const secret = await Vault.createSecret({
  name: "openai",
  value: "sk-...",          // never returned
  provider: "openai",       // supplies scopes
});
console.log(secret.secretId);
v := declaw.NewVaultClient()
defer v.Close()

secret, _ := v.CreateSecret(ctx, declaw.CreateSecretInput{
	Name:     "openai",
	Value:    "sk-...",     // never returned
	Provider: "openai",     // supplies scopes
})
fmt.Println(secret.SecretID)
declaw vault create \
  --name openai \
  --provider openai \
  --value "sk-..."

(b) With explicit scopes

For a custom destination, provide the scopes yourself. domain_regex uses the vault’s ~ prefix to mark a case-insensitive regex match against the request host. The proxy anchors every scope automatically (wrapping it as ^(?:…)$), so a pattern like api.example.com matches that host only and never an attacker-suffixed look-alike (api.example.com.evil.com) — you may still write explicit ^…$ anchors, but you no longer have to. injection_type defaults to bearer; an optional rotation_interval_days enables a rotation policy.
from declaw import VaultClient, VaultScope

vault = VaultClient()

secret = vault.create_secret(
    name="internal-api",
    value="my-token-value",
    scopes=[
        VaultScope(
            domain_regex=r"~^api\.internal\.example\.com$",
            injection_type="header",
            header_name="X-Api-Key",
        ),
    ],
    rotation_interval_days=30,
)
const secret = await Vault.createSecret({
  name: "internal-api",
  value: "my-token-value",
  scopes: [
    {
      domainRegex: "~^api\\.internal\\.example\\.com$",
      injectionType: "header",
      headerName: "X-Api-Key",
    },
  ],
  rotationIntervalDays: 30,
});
secret, _ := v.CreateSecret(ctx, declaw.CreateSecretInput{
	Name:  "internal-api",
	Value: "my-token-value",
	Scopes: []declaw.VaultScope{{
		DomainRegex:   `~^api\.internal\.example\.com$`,
		InjectionType: "header",
		HeaderName:    "X-Api-Key",
	}},
	RotationIntervalDays: 30,
})
# --scope is repeatable: domain_regex,injection_type[,header_name]
declaw vault create \
  --name internal-api \
  --value "my-token-value" \
  --rotation-days 30 \
  --scope '~^api\.internal\.example\.com$,header,X-Api-Key'

List, rotate, and delete secrets

list returns metadata only (id, name, scopes, rotation status) — never the value. rotate replaces the stored value while leaving the scopes unchanged. Running sandboxes pick up the rotated value on their next outbound request (a short-lived TTL cache at the proxy) — no sandbox restart is required. delete removes both the metadata and the stored value.
# List — metadata only
for s in vault.list_secrets():
    print(s.secret_id, s.name, s.rotation_due)

# Rotate — value replaced (by name), scopes unchanged
vault.rotate_secret("internal-api", "sk-new-...")

# Delete — metadata + stored value (by name)
vault.delete_secret("internal-api")
const secrets = await Vault.listSecrets();
for (const s of secrets) console.log(s.secretId, s.name, s.rotationDue);

await Vault.rotateSecret("internal-api", "sk-new-...");

await Vault.deleteSecret("internal-api");
secrets, _ := v.ListSecrets(ctx)
for _, s := range secrets {
	fmt.Println(s.SecretID, s.Name, s.RotationDue)
}

v.RotateSecret(ctx, "internal-api", "sk-new-...")

v.DeleteSecret(ctx, "internal-api")
declaw vault list
declaw vault rotate internal-api --value "sk-new-..."
declaw vault delete internal-api
A secret with rotation_interval_days > 0 is flagged rotation_due in list output once that many days pass since it was last rotated (or created). This is an advisory signal — rotation is not automatic; call rotate to clear it.

Update a secret’s scopes

update_scopes replaces a secret’s injection scopes in place — point it at a new destination or change the injection format — without re-supplying the value. It’s the in-place alternative to delete-and-recreate; running sandboxes pick up the new scopes on their next outbound request.
from declaw import VaultScope

vault.update_scopes("internal-api", [
    VaultScope(domain_regex=r"~^api\.v2\.example\.com$", injection_type="header", header_name="X-Api-Key"),
])
await Vault.updateScopes("internal-api", [
  { domainRegex: "~^api\\.v2\\.example\\.com$", injectionType: "header", headerName: "X-Api-Key" },
]);
v.UpdateScopes(ctx, "internal-api", []declaw.VaultScope{{
    DomainRegex:   `~^api\.v2\.example\.com$`,
    InjectionType: "header",
    HeaderName:    "X-Api-Key",
}})
# --scope is repeatable: domain_regex,injection_type[,header_name]
declaw vault update-scopes internal-api \
  --scope '~^api\.v2\.example\.com$,header,X-Api-Key'

Injection types

A secret’s scope declares how the proxy presents it to the allowed destination. HTTP types inject into the TLS-intercepted request; socket types complete an authentication handshake transparently so the agent connects with no password.
TypeTransportWhat it does
bearerHTTPAuthorization: Bearer <value>
headerHTTP<header_name>: <value> (with optional value_prefix scheme word)
basicHTTPAuthorization: Basic <value> (or base64(username:value) via basic_username)
queryHTTPInject the value as a URL query parameter (header_name names the param)
sigv4HTTPRe-sign the request with AWS SigV4 (stored keys or STS federation)
oidcHTTPMint a short-lived OAuth2/OIDC bearer (client_credentials, token-exchange, discovery)
hmacHTTPSign the request with a configurable HMAC template (GitHub / Stripe / Slack styles)
redisSocketRedis AUTH brokering
postgresSocketPostgres auth brokering (cleartext / MD5 / SCRAM-SHA-256)
mysqlSocketMySQL auth brokering (native + caching_sha2, incl. RSA full-auth)
smtpSocketSMTP AUTH brokering (AUTH PLAIN, STARTTLS)
mongodbSocketMongoDB SCRAM-SHA-256 brokering

Scope fields

A scope is one per-destination injection rule on a secret. The proxy matches a request’s host against domain_regex and injects the value per injection_type. All of the static fields below are applied at the egress proxy after the security scan, so they never appear in the request the agent issued — they let one scope express a provider’s full contract (e.g. Anthropic’s required anthropic-version header) without a new injection mechanism.
FieldRequiredDescription
domain_regexYesHost match. Use the ~ prefix for a case-insensitive regex; the proxy anchors it automatically (^(?:…)$) so it cannot over-match a suffixed host. The value is only injected on requests to a matching host.
injection_typeDefaults to bearerHow the value is presented: bearer, header, basic, query, plus brokered forms sigv4, oidc, hmac, and socket forms redis, postgres, mysql, smtp, mongodb.
header_nameFor header / queryHeader name (e.g. X-Api-Key); for query it is the URL parameter name. Defaults to Authorization for the header type.
value_prefixNoPrepended verbatim to the value for the header type, so a scheme word can be emitted — e.g. "Token "Authorization: Token <key>.
basic_usernameNoFor the basic type: the proxy emits base64(username:value), letting you store a raw key instead of a pre-encoded credential.
extra_headersNoStatic, non-secret headers set alongside the credential (e.g. Anthropic’s required anthropic-version: 2023-06-01).
query_paramsNoStatic, non-secret query parameters added to the request URL. For a secret in the query string, use injection_type: "query" instead.
A single secret may carry multiple scopes — one secret can be injected differently for different destination hosts.

Provider presets

Declaw ships a built-in catalog of ~39 first-party provider templates (OpenAI, Anthropic, Gemini, Bedrock, Cohere, Mistral, Hugging Face, Pinecone, LangSmith, ElevenLabs, Tavily, and more) so you can store a credential by naming the provider and pasting the value — no hand-written domain regex or header (see Storing a secret). The preset expands server-side into the correct scopes at create time; the worker only ever sees the resulting injection specs. Browse the live catalog:
curl https://api.declaw.ai/vault/presets

BYO secret-store connectors

A secret can hold a connector descriptor instead of a literal value — a pointer to your existing secret store. At injection time the worker fetches the real value from that store (cached, per the revocation TTL), so an upstream rotation is picked up automatically on the next cache miss. Implemented connectors: aws_sm · aws_ssm · gcp_sm · azure_kv · vault / openbao · infisical · doppler · k8s · 1password (Connect) · conjur · akeyless The descriptor names the provider and the path to the value; the literal credential never lives in declaw.
Secret values live in OpenBao, never in declaw’s Postgres. The control plane stores only metadata — the secret’s id, name, scopes, and rotation status. The value is written to the vault backend and is fetched worker-side and injected at the proxy. It never enters the control-plane database and is never returned by the API after creation.
Trust boundary and the cert-pinned limitation. Vault HTTP injection rides declaw’s TLS interception at the egress proxy — the same edge proxy used for PII and injection scanning (see Network Policies). The proxy terminates TLS, attaches the credential, and re-originates to the upstream. This means an upstream that certificate-pins (rejects the proxy’s CA) cannot have a credential injected, because the proxy can’t sit in the TLS path. For socket brokers, the agent↔proxy leg is always cleartext within the sandbox’s own network namespace (where the proxy is the only gateway); the proxy↔upstream leg can opt into TLS (set tls: true in the secret value) — smtp upgrades via STARTTLS, redis and mongodb negotiate TLS at connect time, and postgres and mysql upgrade in-protocol — so a database that mandates TLS on the wire is brokered too.