How to provide secrets to a workshop

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; the workshop keeps the connection across refreshes until you disconnect it, remove an endpoint, 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, through the connected plug, each time it asks for the value

The command you run and the processes it starts, 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.

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:

$ 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:

$ 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:

workshop.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:

$ workshop refresh

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:

$ 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:

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

Confirm the connection in the connections listing:

$ 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:

$ 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.

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:

$ 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:

$ 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:

$ 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; logged as secret provider is locked.

Unlock the keyring in your desktop session, then run the command again.

no secret found for plug "<SDK>.<PLUG>"

No item in the collection carries all the slot’s attributes, or the slot names a collection that doesn’t exist; logged as secret not found.

Compare the slot with secret-tool search using the same attributes, correct the slot’s attributes or collection, then run workshop refresh and connect the plug again, because the changed lookup disconnects it.

multiple secrets match plug "<SDK>.<PLUG>"; refine the secret slot

Several items carry all the slot’s attributes; logged as multiple secrets match the request.

List the matching items by running secret-tool search with --all and the slot’s attributes, discarding standard output. Add an attribute to the slot that only the intended item carries, then run workshop refresh and connect the plug again, or remove the other items from the keyring.

secret plug "<SDK>.<PLUG>" is not connected

The plug isn’t connected to the slot; logged as plug is not connected.

Connect it as described in Connect the plug.

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:

$ 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 or bw get password from the Bitwarden CLI.

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

.envrc
export OPENAI_API_KEY=$(secret-tool lookup service openai account work)

Then name the variable in each command:

$ 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:

Reference: