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-toolspackage on Ubuntu, to store and check keyring items.A workshop definition that includes an SDK declaring a
secretplug. 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:
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:
attributesholds at least one attribute with a string value. The lookup matches an item that carries all of them.collectionoptionally selects the keyring collection to search. Without it, the lookup searches the collection that the keyring’sdefaultalias 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 |
|---|---|---|
|
The keyring collection is locked;
logged as |
Unlock the keyring in your desktop session, then run the command again. |
|
No item in the collection carries all the slot’s attributes,
or the slot names a collection that doesn’t exist;
logged as |
Compare the slot with secret-tool search
using the same attributes,
correct the slot’s |
|
Several items carry all the slot’s attributes;
logged as |
List the matching items by running secret-tool search
with |
|
The plug isn’t connected to the slot;
logged as |
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:
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: