<a id="how-provide-secrets"></a>

# How to provide secrets to a workshop

<!-- @artefact secret interface -->
<!-- @artefact workshop exec -->

Some SDKs need a credential at runtime,
such as an API key for an AI coding agent.
**Workshop** can deliver that credential to the workshop
without writing the value into the workshop definition
or the project directory.

## Prerequisites

Before starting, ensure you have these requirements satisfied:

- A desktop session that runs a keyring service
  implementing the freedesktop.org Secret Service,
  such as GNOME Keyring.
  **Workshop** looks the secret up through that session’s D-Bus session bus.
- The **secret-tool** utility,
  shipped in the `libsecret-tools` package on Ubuntu,
  to store and check keyring items.
- A workshop definition that includes an SDK declaring a `secret` plug.
  The SDK’s documentation names the plug
  and the credential it expects.

The OpenAI API key in the examples below is illustrative.
Use the keyring attributes, slot name, and environment variable
expected by your SDK instead.

There are two ways to provide a credential,
so pick one before you start:

|                        | From the host keyring                                                                                                                                                    | For a single command                                                   |
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| What persists          | The value stays in your keyring;<br/>the workshop keeps the connection across refreshes<br/>until you disconnect it, remove an endpoint,<br/>or change the slot’s lookup | Nothing; the next command in the workshop doesn’t see the value        |
| Who can read the value | Any command in the workshop,<br/>through the connected plug,<br/>each time it asks for the value                                                                         | The command you run and the processes it starts,<br/>for that run only |
| Per-run effort         | None once you connect the plug                                                                                                                                           | Name the variable in every command                                     |
| Suits                  | An SDK you use repeatedly                                                                                                                                                | A one-off command that you run yourself                                |

#### WARNING
Don’t put a secret in the workshop definition, its actions,
project files, or shell profiles.
These are plain files:
the project directory, definition included,
is visible inside the workshop and often ends up in version control,
and a shell profile exports its variables to every command you run.

## Use a secret from the host keyring

The host keyring holds the value;
the workshop definition only says where to find it,
and connecting a plug decides whether the workshop can ask for it.

<a id="how-provide-secrets-store"></a>

### Store the credential

Store the credential in the host keyring
under attributes that identify it.
Attributes are name and value pairs you choose;
the workshop finds the item by them,
so pick a combination that no other item shares.
**secret-tool** prompts for the value,
so it never appears on the command line:

```console
$ secret-tool store --label="OpenAI API key" --collection=default service openai account work
```

To check that the item is stored without printing its value,
search by the same attributes and discard standard output.
**secret-tool** writes the value there,
but prints the item’s attributes on standard error:

```console
$ secret-tool search service openai account work >/dev/null

  attribute.account = work
  attribute.service = openai
```

### Add a secret slot

Describe where the secret lives
by adding a `secret` slot to the `system` SDK
in the workshop definition.
The slot carries lookup attributes only, never the value:

```yaml
name: dev
base: ubuntu@24.04
sdks:
  - name: system
    slots:
      openai-key:
        interface: secret
        attributes:
          service: openai
          account: work
  - name: <SDK>
```

Use only these keys in the slot:

- `attributes` holds at least one attribute with a string value.
  The lookup matches an item that carries all of them.
- `collection` optionally selects the keyring collection to search.
  Without it, the lookup searches the collection
  that the keyring’s `default` alias points to,
  which is the Login keyring in GNOME Keyring.
  Any other value is matched against collection labels.
  The label must identify exactly one collection.

Apply the definition with **workshop launch** for a new workshop,
or refresh an existing one:

```console
$ workshop refresh
```

<a id="how-provide-secrets-connect"></a>

### Connect the plug

**Workshop** never connects a `secret` plug on its own:
after you add the slot and launch or refresh,
the plug and the slot stay unconnected until you connect them:

```console
$ workshop connections <WORKSHOP>

  INTERFACE  PLUG                     SLOT                          NOTES
  ...
  secret     -                        <WORKSHOP>/system:openai-key  -
  secret     <WORKSHOP>/<SDK>:<PLUG>  -                             -
```

Connect the SDK’s plug to the slot
to let the workshop ask the keyring for the secret:

```console
$ workshop connect <WORKSHOP>/<SDK>:<PLUG> <WORKSHOP>/system:openai-key
```

Confirm the connection in the connections listing:

```console
$ workshop connections <WORKSHOP>

  INTERFACE  PLUG                     SLOT                          NOTES
  ...
  secret     <WORKSHOP>/<SDK>:<PLUG>  <WORKSHOP>/system:openai-key  manual
```

The connection persists across **workshop refresh**
while the slot’s `attributes` and `collection` stay the same.
A refresh that changes either of them disconnects the plug,
because the new lookup can match a different item,
and reports a new warning;
**workshop warnings** names the dropped connection.
Connect the plug again to approve the new lookup.

To withdraw access, disconnect the plug:

```console
$ workshop disconnect <WORKSHOP>/<SDK>:<PLUG>
```

### Use the secret

Run the SDK’s commands as usual.
Nothing is fetched in advance:
**Workshop** looks the value up in the host keyring
only when an SDK command or service requests it,
and keeps no copy in the workshop.
A direct command receives the value on standard output.
A systemd service receives it in a credential file
that the unit’s user and root can read;
other workshop processes running as the same user can also read that file.
Each request is a fresh lookup,
so a value you change in the keyring takes effect on the next one.
While the plug stays connected,
any command in the workshop can request the value the same way.

<a id="how-provide-secrets-check"></a>

### Troubleshoot failed lookups

A connection doesn’t guarantee that the lookup succeeds,
because **Workshop** asks the keyring only when the value is requested,
and how a failure surfaces depends on the SDK.

Start with the workshop’s record of requests.
Every request creates a recent record in the workshop’s changes,
whether it comes from an SDK command or a service,
so you can check whether and when the SDK received the secret.
**workshop changes** lists each request
as a `Retrieve secret` change named after the plug,
among the other recent changes:

```console
$ workshop changes

  ID  STATUS  SPAWN               READY               SUMMARY
  ...
  44  Done    today at 12:11 UTC  today at 12:11 UTC  Execute command "<COMMAND>"
  45  Error   today at 12:11 UTC  today at 12:11 UTC  Retrieve secret "<WORKSHOP>/<SDK>:<PLUG>"
  ...
  58  Done    today at 12:12 UTC  today at 12:12 UTC  Execute command "<COMMAND>"
  59  Done    today at 12:12 UTC  today at 12:12 UTC  Retrieve secret "<WORKSHOP>/<SDK>:<PLUG>"
```

A `Done` change means that the secret was delivered
to the process that requested it;
`Error` means that it wasn’t.
The `SPAWN` column shows when the request was made.
Neither the changes nor their tasks ever contain the value.

To see why a request failed,
list the tasks of its change;
the end of the logged error names the cause:

```console
$ workshop tasks 45

  STATUS  DURATION  SUMMARY
  Error       47ms  Retrieve secret "<WORKSHOP>/<SDK>:<PLUG>"

  ......................................................................
  Retrieve secret "<WORKSHOP>/<SDK>:<PLUG>"

  2026-10-09T12:11:46Z ERROR getting secret value for sdk "<SDK>" and plug "<PLUG>" in workshop "<WORKSHOP>": resolving secret: provider "system": retrieving system secret: secret provider is locked
```

While **Workshop** retains the change,
the log is available even when the SDK doesn’t show you the error.

To reproduce the failure,
request the value yourself and discard it,
so that only an error reaches your terminal:

```console
$ workshop exec <WORKSHOP> -- workshopctl get-secret <SDK>.<PLUG> > /dev/null

  cannot retrieve secret for plug "<SDK>.<PLUG>": unlock the secret provider and try again
```

No output means the lookup succeeded.
Otherwise, the error names the cause:

| Error                                                                                      | Cause                                                                                                                                                 | Fix                                                                                                                                                                                                                                                                                                                           |
|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `cannot retrieve secret for plug "<SDK>.<PLUG>": unlock the secret provider and try again` | The keyring collection is locked;<br/>logged as `secret provider is locked`.                                                                          | Unlock the keyring in your desktop session,<br/>then run the command again.                                                                                                                                                                                                                                                   |
| `no secret found for plug "<SDK>.<PLUG>"`                                                  | No item in the collection carries all the slot’s attributes,<br/>or the slot names a collection that doesn’t exist;<br/>logged as `secret not found`. | Compare the slot with **secret-tool search**<br/>using the same attributes,<br/>correct the slot’s `attributes` or `collection`,<br/>then run **workshop refresh**<br/>and connect the plug again,<br/>because the changed lookup disconnects it.                                                                             |
| `multiple secrets match plug "<SDK>.<PLUG>"; refine the secret slot`                       | Several items carry all the slot’s attributes;<br/>logged as `multiple secrets match the request`.                                                    | List the matching items by running **secret-tool search**<br/>with `--all` and the slot’s attributes,<br/>discarding standard output.<br/>Add an attribute to the slot that only the intended item carries,<br/>then run **workshop refresh**<br/>and connect the plug again,<br/>or remove the other items from the keyring. |
| `secret plug "<SDK>.<PLUG>" is not connected`                                              | The plug isn’t connected to the slot;<br/>logged as `plug is not connected`.                                                                          | Connect it as described in [Connect the plug](#how-provide-secrets-connect).                                                                                                                                                                                                                                                  |

Any other failure prints an error without details;
find its cause in the task log of the failed change.

## Pass a secret to a single command

To give a one-off command a credential
without adding a slot or connecting a plug,
pass it ad hoc as an environment variable
of a single **workshop exec** or **workshop run** invocation.
This works with any credential store on the host;
what keeps it safe is that the value never appears on the command line.

Give the `--env` flag only the variable’s name;
**Workshop** then takes the value from the environment
of the **workshop** command itself.
Don’t type the value, as in `--env OPENAI_API_KEY=<VALUE>`:
it stays in your shell history,
and other processes on the host can read it from the process list
while the command runs.

Instead, fill the variable from where the credential is already stored,
with a command substitution
in an assignment that prefixes the command.
For an item in the host keyring:

```console
$ OPENAI_API_KEY=$(secret-tool lookup service openai account work) workshop exec --env OPENAI_API_KEY <WORKSHOP> -- <COMMAND>
```

The shell history records the lookup, not the value,
and the assignment applies only to that command,
so the variable doesn’t remain in your shell afterwards.

A password manager’s CLI works the same way
once you’ve signed in or unlocked the vault:
put its lookup in the substitution instead,
such as **op read** from the
[1Password CLI](https://www.1password.dev/cli/reference/commands/read)
or **bw get password** from the
[Bitwarden CLI](https://bitwarden.com/help/cli/).

To avoid repeating the lookup in every command,
you can let a tool such as [direnv](https://direnv.net/)
set the variable whenever you enter the project directory on the host,
with an `.envrc` file that runs the same lookup:

```shell
export OPENAI_API_KEY=$(secret-tool lookup service openai account work)
```

Then name the variable in each command:

```console
$ workshop exec --env OPENAI_API_KEY <WORKSHOP> -- <COMMAND>
```

The `.envrc` file lives in the project directory,
which is visible inside the workshop and may end up in version control,
so it must hold only the lookup, never the value.
Also, every command you run in that directory on the host
inherits the variable.

If the named variable isn’t set,
**Workshop** skips it without an error
and runs the command without it.
Otherwise, the value exists only for that invocation:
the command and every process it starts can read it,
and the next command in the workshop doesn’t see it.

## See also

Explanation:

- [Plugs and slots](https://ubuntu.com/workshop/docs/explanation/interfaces/plugs-and-slots.md#exp-plugs-slots)

Reference:

- [workshop changes](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-changes)
- [workshop connect](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-connect)
- [workshop connections](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-connections)
- [workshop disconnect](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-disconnect)
- [workshop exec](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-exec)
- [workshop run](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-run)
- [workshop tasks](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-tasks)
- [workshop warnings](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-warnings)
- [workshopctl (CLI)](https://ubuntu.com/workshop/docs/reference/cli/workshopctl.md#ref-workshopctl-cli)
