# Give a tool a secret

> Declare the secrets a tool needs and read them from the call's context, resolved by Kindgi for each call.

This guide gives a tool an API key or a signing key, so its handler gets the value on every call without it appearing in your code or config.

## Before you start

- **A pack,** on a runtime such as `kindgi dev`, with Kindgi 0.1's CLI. This guide uses the quickstart's pack, `my-pack`.
- **A flow that runs the tool,** to try it. This guide uses `my-pack.sign`, a one-step flow.
- **`openssl`,** to make a key for the example.

## Declare the secret

A tool that needs a key declares it by name. Kindgi resolves it on every call, checks it, and hands it to the handler in the call's context.

Ask your coding agent: "I want a tool that signs an order receipt with a key, so our customer portal can verify it." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.10/start/coding-agents.md)). You store the key itself, as [Store the secret](https://docs.kindgi.com/v0.1.10/guides/tools/give-a-tool-a-secret.md#store-the-secret) shows. For reference, the tool looks like this:

- **TypeScript**

  `tools/sign-receipt/index.ts`

  ```ts
  import { createHmac } from 'node:crypto';

  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const defined = defineTool({
    id: 'my-pack.sign-receipt' as ToolId,
    description: 'Signs an order receipt so the customer portal can verify it.',
    version: '0.1.0',
    input: z.object({ orderId: z.string(), totalCents: z.number().int() }),
    output: z.object({ signature: z.string() }),
    effects: [],
    mutating: false,
    needsSpec: {
      secrets: { RECEIPT_SIGNING_KEY: { type: 'string', minLength: 32 } },
    },
    handler: async ({ orderId, totalCents }, ctx) => {
      const key = ctx.secrets?.RECEIPT_SIGNING_KEY;
      if (key === undefined) throw new Error('RECEIPT_SIGNING_KEY is not set');
      const signature = createHmac('sha256', key).update(`${orderId}:${totalCents}`).digest('hex');
      return { signature };
    },
  });

  if (defined.kind === 'err') {
    throw new Error(`my-pack.sign-receipt failed to compile: ${defined.error.message}`);
  }

  export default defined.value;
  ```

- **Python**

  `tools/receipts.py`

  ```python
  import hashlib
  import hmac

  from pydantic import BaseModel, Field

  from kindgi import ToolContext, tool


  class Receipt(BaseModel):
      order_id: str = Field(alias="orderId")
      total_cents: int = Field(alias="totalCents")


  class Signed(BaseModel):
      signature: str


  @tool(
      id="my-pack.sign-receipt",
      mutating=False,
      needs_spec={"secrets": {"RECEIPT_SIGNING_KEY": {"type": "string", "minLength": 32}}},
  )
  def sign_receipt(receipt: Receipt, ctx: ToolContext) -> Signed:
      """Signs an order receipt so the customer portal can verify it."""
      key = ctx.secrets["RECEIPT_SIGNING_KEY"].encode()
      message = f"{receipt.order_id}:{receipt.total_cents}".encode()
      return Signed(signature=hmac.new(key, message, hashlib.sha256).hexdigest())
  ```

- **Java**

  `src/main/java/mypack/tools/SignReceipt.java`

  ```java
  package mypack.tools;

  import com.kindgi.pack.Tool;
  import java.nio.charset.StandardCharsets;
  import java.util.HexFormat;
  import java.util.Map;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  public final class SignReceipt {
    public record Input(String orderId, long totalCents) {}

    public record Output(String signature) {}

    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.sign-receipt")
        .description("Signs an order receipt so the customer portal can verify it.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .mutating(false)
        .set("needsSpec", Map.of(
            "secrets", Map.of("RECEIPT_SIGNING_KEY", Map.of("type", "string", "minLength", 32))))
        .handler((input, ctx) -> {
          Object key = ctx.secrets().get("RECEIPT_SIGNING_KEY");
          if (key == null) throw new IllegalStateException("RECEIPT_SIGNING_KEY is not set");
          Mac mac = Mac.getInstance("HmacSHA256");
          mac.init(new SecretKeySpec(key.toString().getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
          byte[] signed = mac.doFinal((input.orderId() + ":" + input.totalCents()).getBytes(StandardCharsets.UTF_8));
          return new Output(HexFormat.of().formatHex(signed));
        });

    private SignReceipt() {}
  }
  ```

  `needsSpec` goes in with `set("needsSpec", …)`, as the API names it. `ctx.secrets()` holds the declared secrets, by name.

- **Scala**

  `src/main/scala/mypack/tools/SignReceipt.scala`

  ```scala
  package mypack.tools

  import com.kindgi.pack.scaladsl._
  import java.nio.charset.StandardCharsets.UTF_8
  import java.util.HexFormat
  import javax.crypto.Mac
  import javax.crypto.spec.SecretKeySpec

  object SignReceipt {
    final case class Input(orderId: String, totalCents: Long)
    final case class Output(signature: String)

    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.sign-receipt")
      .description("Signs an order receipt so the customer portal can verify it.")
      .version("0.1.0")
      .readOnly
      .set("needsSpec", Map(
        "secrets" -> Map("RECEIPT_SIGNING_KEY" -> Map("type" -> "string", "minLength" -> 32))))
      .handler { (in, ctx) =>
        val key = Option(ctx.secrets.get("RECEIPT_SIGNING_KEY"))
          .getOrElse(throw new IllegalStateException("RECEIPT_SIGNING_KEY is not set"))
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(new SecretKeySpec(key.toString.getBytes(UTF_8), "HmacSHA256"))
        Output(HexFormat.of().formatHex(mac.doFinal(s"${in.orderId}:${in.totalCents}".getBytes(UTF_8))))
      }
  }
  ```

  `needsSpec` goes in with `set("needsSpec", …)`, as the API names it. `ctx.secrets` holds the declared secrets, by name.

* **The declaration** maps each secret's name to a JSON Schema for its value. A declared secret is required, unless its schema accepts `null`, as [An optional secret](https://docs.kindgi.com/v0.1.10/guides/tools/give-a-tool-a-secret.md#an-optional-secret) shows. Each schema must compile as the runtime compiles it, strictly: a tool with an unknown keyword is refused when it's defined, registered or deployed.
* **On every call**, Kindgi resolves each declared secret for the call's tenant, and checks it against its schema. It reads it in the env the runtime serves, which `KINDGI_ENV` names. Under `kindgi dev`, the env is `local`, and the secrets are in the pack's env files. If one is missing or doesn't fit, the call fails before the handler runs.
* **The context** holds only what the tool declares. `ctx.secrets` is optional in the TypeScript type because a unit test builds its own context.

## An optional secret

A tool that can do without a secret declares it with a schema that accepts `null`: `{ "type": ["string", "null"] }`. Here `my-pack.sign-receipt` also stamps the receipt when a stamp key is set. When the env doesn't have it, or has it empty, it's left out of `ctx.secrets`, and the call goes on. The call's log line names it:

```text excerpt
tool my-pack.sign-receipt: optional secret RECEIPT_STAMP_KEY not set in env "local"; the call goes on without …
```

A value that is set is still checked against the schema. Only a secret never stored, or an empty one, counts as not set: a revoked secret, one whose value is gone at its provider though it's still mapped, or a secrets backend that fails, still fails the call. The schema has to name `null`: an unconstrained `{}` stays required.

Ask your coding agent: "Also stamp the receipt when a stamp key is set." For reference, the change looks like this:

- **TypeScript**

  ```ts excerpt
  needsSpec: {
    secrets: {
      RECEIPT_SIGNING_KEY: { type: 'string', minLength: 32 },
      RECEIPT_STAMP_KEY: { type: ['string', 'null'], minLength: 32 }, // optional
    },
  },
  handler: async (receipt, ctx) => {
    const stampKey = ctx.secrets?.RECEIPT_STAMP_KEY; // undefined when not set
    // …
  },
  ```

- **Python**

  ```python excerpt
  needs_spec={
      "secrets": {
          "RECEIPT_SIGNING_KEY": {"type": "string", "minLength": 32},
          "RECEIPT_STAMP_KEY": {"type": ["string", "null"], "minLength": 32},  # optional
      }
  }
  # …
  stamp_key = ctx.secrets.get("RECEIPT_STAMP_KEY")  # None when not set
  ```

- **Java**

  ```java excerpt
  .set("needsSpec", Map.of("secrets", Map.of(
      "RECEIPT_SIGNING_KEY", Map.of("type", "string", "minLength", 32),
      "RECEIPT_STAMP_KEY", Map.of("type", List.of("string", "null"), "minLength", 32)))) // optional
  .handler((receipt, ctx) -> {
    Object stampKey = ctx.secrets().get("RECEIPT_STAMP_KEY"); // null when not set
    // …
  });
  ```

- **Scala**

  ```scala excerpt
  .set("needsSpec", Map("secrets" -> Map(
    "RECEIPT_SIGNING_KEY" -> Map("type" -> "string", "minLength" -> 32),
    "RECEIPT_STAMP_KEY" -> Map("type" -> List("string", "null"), "minLength" -> 32)))) // optional
  .handler { (receipt, ctx) =>
    val stampKey = Option(ctx.secrets.get("RECEIPT_STAMP_KEY")) // None when not set
    // …
  }
  ```

> **Note: Runtime 0.1.6 or later**
>
> An older runtime requires every declared secret, optional ones included: a call without one fails with `secret-unavailable`, naming it, and an empty one is checked against its schema as `""` (with a `minLength`, `secret-invalid`).

## Store the secret

Until the secret exists, calls fail and name it:

```json excerpt
  "status": "failed",
  "failureMessage": "handler-error: Tool \"my-pack.sign-receipt\" handler threw: secret-unavailable: tool \"my-pack.sign-receipt\" needs secret \"RECEIPT_SIGNING_KEY\" in env \"local\": No secret \"RECEIPT_SIGNING_KEY\" for env \"local\" in .env, .env.local, .kindgi/secrets.env at /pack",
```

Store a value. `kindgi secrets set` prompts for it without echoing it. Here it reads it from stdin instead:

- **TypeScript**

  ```sh
  openssl rand -hex 32 | tr -d '\n' | pnpm exec kindgi secrets set RECEIPT_SIGNING_KEY --env=local --scope=tenant --from-stdin
  ```

- **Python**

  ```sh
  openssl rand -hex 32 | tr -d '\n' | uv run kindgi secrets set RECEIPT_SIGNING_KEY --env=local --scope=tenant --from-stdin
  ```

- **Java**

  ```sh
  openssl rand -hex 32 | tr -d '\n' | ./kindgiw secrets set RECEIPT_SIGNING_KEY --env=local --scope=tenant --from-stdin
  ```

- **Scala**

  ```sh
  openssl rand -hex 32 | tr -d '\n' | ./kindgiw secrets set RECEIPT_SIGNING_KEY --env=local --scope=tenant --from-stdin
  ```

It prints the secret's name, scope, environment and version as JSON, then:

```text excerpt
…
  Set RECEIPT_SIGNING_KEY at tenant in local.
```

- **`openssl rand -hex 32 | tr -d '\n'`** makes a 64-character key, without a newline.
- **`--env`** is the env the runtime serves. Under `kindgi dev`, it's `local`.
- **`--scope=tenant`** stores it for the whole tenant.
- **`--from-stdin`** reads the value from the pipe instead of prompting.

Under `kindgi dev`, that writes it to Kindgi's own `.kindgi/secrets.env`. The next call gets it, and nothing restarts. Run the tool from `my-pack.sign`. `--flow` is the flow, and `--input` its input:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.sign --input='{"orderId":"ord_1001","totalCents":4200}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.sign --input='{"orderId":"ord_1001","totalCents":4200}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.sign --input='{"orderId":"ord_1001","totalCents":4200}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.sign --input='{"orderId":"ord_1001","totalCents":4200}'
  ```

```json excerpt
  "status": "completed",
  …
  "output": {
    "signature": "b02d6ae82a21be7754b7c605ae55210fdb2d6c14ca25f03b56c0d62c6f27e595"
  },
```

A value that doesn't fit the schema fails the call too, without the value in the message:

```json excerpt
  "failureMessage": "handler-error: Tool \"my-pack.sign-receipt\" handler threw: secret-invalid: secret \"RECEIPT_SIGNING_KEY\" in env \"local\" doesn't match the schema tool \"my-pack.sign-receipt\" declares for it: must NOT have fewer than 32 characters",
```

To replace a value, `set` it again with `--write-mode=add-version`. Without it, `set` refuses a name that exists. [Store a secret](https://docs.kindgi.com/v0.1.10/guides/secrets/store-a-secret.md) covers `kindgi secrets`.

## Test it

A test passes the secret in the context it builds. Ask your coding agent: "Add a test for the signing tool, with a made-up key." For reference, the test looks like this:

- **TypeScript**

  `tools/sign-receipt/index.test.ts`

  ```ts
  import { invokeTool } from '@kindgi/sdk/define';
  import type { TenantId } from '@kindgi/sdk/types';
  import { expect, test } from 'vitest';

  import signReceipt from './index.js';

  test('signs with the key from the context', async () => {
    const result = await invokeTool(
      signReceipt,
      { orderId: 'ord_1001', totalCents: 4200 },
      {
        tenantId: 'test-tenant' as TenantId,
        abortSignal: new AbortController().signal,
        secrets: { RECEIPT_SIGNING_KEY: 'k'.repeat(32) },
      },
    );
    expect(result.kind).toBe('ok');
  });
  ```

- **Python**

  `tests/test_receipts.py`

  ```python
  from kindgi import ToolContext
  from tools.receipts import Receipt, sign_receipt


  def test_signs_with_the_key_from_the_context():
      ctx = ToolContext.for_test(secrets={"RECEIPT_SIGNING_KEY": "k" * 32})
      signed = sign_receipt(Receipt(orderId="ord_1001", totalCents=4200), ctx)
      assert len(signed.signature) == 64
  ```

- **Java**

  `src/test/java/mypack/tools/SignReceiptTest.java`

  ```java
  package mypack.tools;

  import static org.junit.jupiter.api.Assertions.assertEquals;

  import com.kindgi.log.Logger;
  import com.kindgi.pack.Cancellation;
  import com.kindgi.pack.ToolContext;
  import java.util.Map;
  import org.junit.jupiter.api.Test;

  class SignReceiptTest {
    @Test
    void signsWithTheKeyFromTheContext() throws Exception {
      ToolContext ctx = new ToolContext("test-tenant", "test-run", null, null, null, Map.of(),
          Map.of("RECEIPT_SIGNING_KEY", "k".repeat(32)), Map.of(), Map.of(), new Cancellation(), Logger.noop());
      SignReceipt.Output output = SignReceipt.TOOL.call(new SignReceipt.Input("ord_1001", 4200), ctx);
      assertEquals(64, output.signature().length());
    }
  }
  ```

  `ToolContext.forTest()` has no secrets: build the context with `new ToolContext(…)`, which takes them. `TOOL.call` runs the handler alone, without the schema checks.

- **Scala**

  `src/test/scala/mypack/tools/SignReceiptSuite.scala`

  ```scala
  package mypack.tools

  import com.kindgi.log.Logger
  import com.kindgi.pack.{Cancellation, ToolContext}
  import java.util.Map as JMap

  class SignReceiptSuite extends munit.FunSuite {
    test("signs with the key from the context") {
      val ctx = new ToolContext("test-tenant", "test-run", null, null, null, JMap.of(),
        JMap.of("RECEIPT_SIGNING_KEY", "k" * 32), JMap.of(), JMap.of(), new Cancellation(), Logger.noop())
      val output = SignReceipt.tool.call(SignReceipt.Input("ord_1001", 4200), ctx)
      assertEquals(output.signature.length, 64)
    }
  }
  ```

  `ToolContext.forTest()` has no secrets: build the context with `new ToolContext(…)`, which takes them. `tool.call` runs the handler alone, without the schema checks.

## A model provider's key isn't a tool's

A model provider's key is used by its provider only. A tool can't declare or send a secret that a provider registration of the tenant names, in any environment. Here the tenant has Anthropic registered from its preset, with the key `ANTHROPIC_API_KEY`. A tool, `acme.notes.summarize`, declares `ANTHROPIC_API_KEY` too.

**Under `kindgi dev`**, the tool loads, and each call fails without running it:

```json excerpt
  "status": "failed",
  "failureMessage": "precondition-failed: Tool \"acme.notes.summarize\" was not run: provider-key-refused: `ANTHROPIC_API_KEY` is the key of model provider \"anthropic\": a tool never gets a model provider's key. If it needs to call a model itself, store the key under its own name (the same value is fine) and use that name",
```

**On a deployed runtime**, it isn't registered at all. Each of these is refused in the same words:

- `POST /v1/tools` answers `400`.
- A deploy that brings the tool fails with a `deployment-validation-failed` issue for it.
- Reinstating a retired version that names the key is refused.

The error reads:

```json excerpt
{"error":{"code":"provider-key-refused","message":"`ANTHROPIC_API_KEY` is the key of model provider \"anthropic\": a tool never gets a model provider's key. If it needs to call a model itself, store the key under its own name (the same value is fine) and use that name","details":{"secret":"ANTHROPIC_API_KEY","providerId":"anthropic"},"requestId":"req-…"}}
```

So a pack you install can't take your model key. A tool that calls a model itself declares a key under a name of its own, and you store the key there, the same value if you like:

- **TypeScript**

  ```sh
  pnpm exec kindgi secrets set SUMMARIZER_ANTHROPIC_KEY --env=local --scope=tenant
  ```

- **Python**

  ```sh
  uv run kindgi secrets set SUMMARIZER_ANTHROPIC_KEY --env=local --scope=tenant
  ```

- **Java**

  ```sh
  ./kindgiw secrets set SUMMARIZER_ANTHROPIC_KEY --env=local --scope=tenant
  ```

- **Scala**

  ```sh
  ./kindgiw secrets set SUMMARIZER_ANTHROPIC_KEY --env=local --scope=tenant
  ```

It prompts for the value without echoing it. It prints the secret's name, scope, environment and version as JSON, then:

```text excerpt
…
  Set SUMMARIZER_ANTHROPIC_KEY at tenant in local.
```

With `SUMMARIZER_ANTHROPIC_KEY` in its `needsSpec.secrets`, the same call completes:

```json excerpt
  "status": "completed",
  …
    "summary": "Ship the provider-key guard in 0.1.6."
```

The same holds for an MCP endpoint's and a webhook endpoint's `secretRef`.

## Why not the process environment

The pack service is one process for every tenant your runtime serves, so its environment can't hold a key that belongs to one of them. Keep `process.env` or `os.environ` for your app's own settings, such as a service URL or a database your app owns. Declare them as the pack's environment, as [Declare the environment your code reads](https://docs.kindgi.com/v0.1.10/guides/secrets/pack-env.md) shows.

> **Caution**
>
> Under `kindgi dev`, a secret you store with `kindgi secrets set` isn't in the pack service's environment, and no model provider's key is, whichever env file holds it: code that reads one from `process.env` finds nothing. Read a secret you stored from `ctx.secrets`. A provider's key is for its provider only. Values in your app's own env files are in that environment: they're your app's settings.

An [HTTP tool](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#send-a-credential) names its credential in `authorization.secretRef` instead, and Kindgi adds it to the request.

## Next

- [Store a secret](https://docs.kindgi.com/v0.1.10/guides/secrets/store-a-secret.md)
- [Give a tool per-project values](https://docs.kindgi.com/v0.1.10/guides/tools/give-a-tool-env-values.md)
- [Call an HTTP API without code](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md)
