> ## Documentation Index
> Fetch the complete documentation index at: https://www.kernel.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Passkeys

> Register, import, and sign in with passkeys stored in a credential item, without the private key entering the browser

a `credential` item can hold passkeys in `passkey` fields. a browser attached to the item's vault can then create a passkey on a website and sign in with it. KERNEL answers the page's WebAuthn requests (`navigator.credentials.create` and `navigator.credentials.get`) and signs on its servers, so the private key never enters the browser, your application, or the model's context.

passkeys aren't filled into pages. instead of `fill`, use three item operations:

| operation | what it does |
| - | - |
| `register_passkey` | the next passkey a page creates in the browser is generated by KERNEL and stored in an empty passkey field |
| `arm_passkey` | the browser answers sign-in requests for the passkey's website with a stored passkey |
| `disarm_passkey` | the browser stops using the item's passkeys |

each operation returns immediately and has no time limit. your agent drives the website with its own browser tools; KERNEL only answers the page's passkey requests.

> [!WARNING]
>
> while a passkey is armed, any script running on that website's pages can obtain a sign-in without user interaction, as with a password manager that fills automatically. disarm as soon as the task is done.

## Requirements

* a `credential` item with `provider: "kernel"`. see [credential items](https://www.kernel.sh/docs/vaults/credentials.md).
* a browser created with the item's vault attached. use the browser's session id in `browser_id`, not its name.
* invoke an operation only when the item advertises it in `available_operations`. KERNEL rechecks eligibility when you invoke it.
* SDK 0.131.0 or later for Node, Python, and Go, or CLI 0.50.0 or later.

## Add a passkey field

a passkey field must be sensitive. add an optional, empty passkey field to an existing item with an update. a required passkey field needs a value when you add it, so use an optional field when KERNEL will create the passkey.

```typescript TypeScript theme={null}
const current = await kernel.vaults.items.retrieve("portal-login", {
  id_or_name: vault.id,
});
if (current.type !== "credential") throw new Error("not a credential item");
const item = await kernel.vaults.items.update(current.key, {
  id_or_name: vault.id,
  type: "credential",
  version: current.version,
  expected_item_id: current.id,
  spec: {
    add_fields: [
      {
        name: "passkey",
        label: "Passkey",
        type: "passkey",
        required: false,
        sensitive: true,
      },
    ],
  },
});
```

```python Python theme={null}
current = kernel.vaults.items.retrieve("portal-login", id_or_name=vault.id)
if current.type != "credential":
    raise RuntimeError("not a credential item")
item = kernel.vaults.items.update(
    current.key,
    id_or_name=vault.id,
    type="credential",
    version=current.version,
    expected_item_id=current.id,
    spec={
        "add_fields": [
            {
                "name": "passkey",
                "label": "Passkey",
                "type": "passkey",
                "required": False,
                "sensitive": True,
            }
        ]
    },
)
```

you can also declare the passkey field when you create the item. collection forms leave passkey fields out, so the item becomes ready once its other required fields have values.

## Register a passkey on a website

invoke `register_passkey` with the browser's session id, then have your agent open the website's "add a passkey" flow in that browser. when the page calls `navigator.credentials.create`, KERNEL generates the passkey, stores it in the field, and only then returns it to the page. the website never receives a passkey that KERNEL failed to store.

```typescript TypeScript theme={null}
const registration = await kernel.vaults.items.performOperation("portal-login", {
  id_or_name: vault.id,
  type: "register_passkey",
  browser_id: browser.session_id,
  field: "passkey",
});
// drive the website's "add a passkey" flow with your browser tools, then confirm:
const registered = await kernel.vaults.items.retrieve("portal-login", {
  id_or_name: vault.id,
});
if (
  registered.type !== "credential" ||
  registered.state.provider !== "kernel" ||
  !registered.state.fields.passkey?.has_value
) {
  throw new Error("the website didn't create a passkey");
}
```

```python Python theme={null}
registration = kernel.vaults.items.perform_operation(
    "portal-login",
    id_or_name=vault.id,
    type="register_passkey",
    browser_id=browser.session_id,
    field="passkey",
)
# drive the website's "add a passkey" flow with your browser tools, then confirm:
registered = kernel.vaults.items.retrieve("portal-login", id_or_name=vault.id)
if (registered.type != "credential" or registered.state.provider != "kernel"
        or not registered.state.fields["passkey"].has_value):
    raise RuntimeError("the website didn't create a passkey")
```

`field` names an empty passkey field. omit it when the item has exactly one empty passkey field. the registration waits for the next passkey any page in that browser creates, so start it right before the website's flow. each browser has one pending registration; a later `register_passkey` replaces it.

after the website confirms the new passkey, the field reports `has_value` and the vault records a `credential_passkey_registered` event. sign in once with `arm_passkey` to confirm the website accepts it. KERNEL creates ES256 passkeys only; a website that doesn't accept ES256 gets a `NotSupportedError`.

to replace a stored passkey, clear the field or add another empty passkey field first. `register_passkey` only writes into an empty field.

## Import an existing passkey

if you already hold a passkey, for example exported from a password manager, write it as the field's value from your trusted backend. the value is a JSON-encoded passkey object from the FIDO Credential Exchange Format (CXF):

| member | value |
| - | - |
| `credentialId` | unpadded base64url credential id |
| `userHandle` | unpadded base64url user handle, at most 64 bytes |
| `key` | unpadded base64url PKCS#8 ECDSA P-256 private key |
| `rpId` | lowercase relying party domain, such as `example.com` |
| `username`, `userDisplayName` | optional display names |

other CXF members are ignored. keep the private key in trusted backend code; don't put it in frontend code, agent prompts, command-line arguments, logs, or traces.

```typescript TypeScript theme={null}
const exported = process.env.PORTAL_PASSKEY_CXF;
if (!exported) throw new Error("an exported passkey is required");

const current = await kernel.vaults.items.retrieve("portal-login", {
  id_or_name: vault.id,
});
if (current.type !== "credential") throw new Error("not a credential item");
await kernel.vaults.items.update(current.key, {
  id_or_name: vault.id,
  type: "credential",
  version: current.version,
  expected_item_id: current.id,
  spec: { fields: { passkey: { value: exported } } },
});
```

```python Python theme={null}
import os

exported = os.environ["PORTAL_PASSKEY_CXF"]

current = kernel.vaults.items.retrieve("portal-login", id_or_name=vault.id)
if current.type != "credential":
    raise RuntimeError("not a credential item")
kernel.vaults.items.update(
    current.key,
    id_or_name=vault.id,
    type="credential",
    version=current.version,
    expected_item_id=current.id,
    spec={"fields": {"passkey": {"value": exported}}},
)
```

item reads never return the passkey. they report `has_value` for the field.

## Sign in with a passkey

invoke `arm_passkey`, then have your agent sign in on the website as a user would, for example by selecting **Sign in with a passkey**. when a page in that browser whose origin matches the passkey's relying party asks for a passkey, KERNEL answers with the armed passkey. this includes autofill requests that pages make as soon as they load.

```typescript TypeScript theme={null}
const armed = await kernel.vaults.items.performOperation("portal-login", {
  id_or_name: vault.id,
  type: "arm_passkey",
  browser_id: browser.session_id,
});
if (armed.type !== "arm_passkey") throw new Error("unexpected operation response");
console.log(armed.status, armed.rp_id); // "armed", "example.com"
// sign in with your browser tools and inspect the page to confirm it.
```

```python Python theme={null}
armed = kernel.vaults.items.perform_operation(
    "portal-login",
    id_or_name=vault.id,
    type="arm_passkey",
    browser_id=browser.session_id,
)
if armed.type != "arm_passkey":
    raise RuntimeError("unexpected operation response")
print(armed.status, armed.rp_id)  # "armed", "example.com"
# sign in with your browser tools and inspect the page to confirm it.
```

`status: "armed"` means the browser will answer matching requests; it doesn't mean a page has asked yet. pass `field` when the item has more than one passkey field holding a passkey.

KERNEL builds each response from the origin Chrome reports for the requesting frame, not from what the page claims, and answers only pages on the passkey's relying party. if the website lists the credentials it accepts, only a matching passkey answers. the vault records a `credential_passkey_asserted` event for each sign-in.

a page opened in a new tab or window can run its first scripts before KERNEL's handler attaches. if such a page asks for a passkey as soon as it loads, reload it.

## Disarm

invoke `disarm_passkey` when the task is done. it removes the browser's armed passkeys and its pending registration for the item. pass `field` to disarm one passkey field; omit it to disarm all of them.

```typescript TypeScript theme={null}
await kernel.vaults.items.performOperation("portal-login", {
  id_or_name: vault.id,
  type: "disarm_passkey",
  browser_id: browser.session_id,
});
```

```python Python theme={null}
kernel.vaults.items.perform_operation(
    "portal-login",
    id_or_name=vault.id,
    type="disarm_passkey",
    browser_id=browser.session_id,
)
```

pages that already loaded keep KERNEL's handler, but they get the browser's own passkey behavior after you disarm.

an arm also ends when the browser ends, the vault is detached, or the item or field is deleted.

## Use the CLI

```bash theme={null}
kernel vaults items invoke user-12345 portal-login register_passkey --browser-id <browser-session-id> --field passkey
kernel vaults items invoke user-12345 portal-login arm_passkey --browser-id <browser-session-id>
kernel vaults items invoke user-12345 portal-login disarm_passkey --browser-id <browser-session-id>
```

## Behavior and limits

* arming a passkey for a website replaces any passkey armed earlier in the same browser for that website.
* passkeys report a signature counter of 0 and set the backup flags, as synced passkeys do. websites don't flag repeated sign-ins from different browsers as a cloned authenticator.
* passkeys are never filled. `fill` rejects passkey fields, and passkey fields can't be bindings for `webmcp_invoke`.
* KERNEL-hosted collection forms and [`@onkernel/vault-react`](https://www.npmjs.com/package/@onkernel/vault-react) 0.4.0 and later leave passkey fields out. vault-react 0.2.0 can't load an item that has a passkey field, so upgrade a custom form before adding one.
* passkeys work only in browsers whose image supports them. on an older browser, `arm_passkey` and `register_passkey` return `409` with "browser does not support passkeys".


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.