How to use secrets in an SDK

An SDK that needs a credential at runtime, such as an API key, declares a secret plug. After a user connects the plug to a secret slot, the SDK can request the value when it is needed without shipping or storing the value itself.

Use this guide to add one of the supported delivery patterns to an existing SDK.

Prerequisites

Before starting, ensure you have these requirements satisfied:

  • An existing SDKcraft project.

  • A command, tool, or systemd service in the SDK that needs a credential at runtime.

  • A host keyring and the secret-tool utility to store a test value, as listed in How to provide secrets to a workshop.

Choose how the SDK receives the value

Choose the delivery mechanism based on what consumes the credential:

What needs it

Mechanism

Why

A tool with a credential-helper setting

workshopctl get-secret, called directly by the tool

The tool requests the value only when needed without putting it in the tool’s environment

A command that reads standard input

Pipe from workshopctl get-secret

The value can stay out of the command’s environment

A command that requires an environment variable

An SDK-supplied wrapper

The variable exists only in the wrapper and the process it starts

A long-running systemd service

LoadCredential= in the service unit

systemd requests the value on each start and makes it available as a credential file

A value the user passes for a single command

The user’s --env argument

The user supplies the value ad hoc, without a slot or a connection

Warning

Never put a secret in SDK content, sdkcraft.yaml, workshop definitions, hooks, shell profiles, examples, or logs. Each of these is a file that ships, persists, or is shared, so a value written there outlives the moment it was needed.

Declare a secret plug

Add a secret plug to sdkcraft.yaml. The plug has a name and an interface, but no lookup attributes or value:

sdkcraft.yaml
name: secret-demo
# ...
plugs:
  api-key:
    interface: secret

Only regular SDKs can declare secret plugs.

Inside the workshop, the request identifier is <SDK>.<PLUG>; for an SDK named secret-demo with the plug above, the identifier is secret-demo.api-key.

Workshop never connects a secret plug automatically. Document the plug name and the credential it expects so users know which slot to create and connect. The SDK must also handle the plug being unconnected or the lookup failing.

Connect the plug in a test workshop

The plug has nothing to request until it’s connected to a secret slot. Slots don’t ship with the SDK: they belong to the system SDK, and each user adds one to their workshop definition, with lookup attributes that identify an item in their own keyring. To test the SDK, take the same steps with a throwaway value.

Store the test value in the host keyring. secret-tool prompts for the value; enter the 23-byte not-a-real-secret-value to match the output of later steps:

$ secret-tool store --label="Secret demo test key" --collection=default service secret-demo account test

Build and install the SDK with sdkcraft try, as described in Try the SDK:

$ sdkcraft try

In the test workshop definition, add a secret slot to the system SDK with attributes that match the stored item, and list the SDK with the try- prefix:

workshop.yaml
name: dev
base: ubuntu@24.04
sdks:
  - name: system
    slots:
      demo-key:
        interface: secret
        attributes:
          service: secret-demo
          account: test
  - name: try-secret-demo

Launch the workshop:

$ workshop launch

Then connect the plug to the slot. The plug keeps the SDK’s own name, without the try- prefix:

$ workshop connect dev/secret-demo:api-key dev/system:demo-key

The connection persists when you rebuild the SDK and refresh the workshop in later steps.

Request a secret on demand

Run workshopctl get-secret <SDK>.<PLUG> inside the workshop to request a connected secret:

$ workshopctl get-secret <SDK>.<PLUG>

The command writes the value to standard output. Capture or pipe that output directly into the consuming tool; do not print it to a terminal or log.

For example, the following check counts the bytes of the test value without displaying it:

$ workshop exec dev -- workshopctl get-secret secret-demo.api-key | wc -c

  23

When a request fails, workshopctl writes a short diagnostic to standard error and exits with a status that identifies the failure. To see one, disconnect the plug:

$ workshop disconnect dev/secret-demo:api-key

Then repeat the request; an unconnected plug returns status 3:

$ workshop exec dev -- workshopctl get-secret secret-demo.api-key

  secret plug "secret-demo.api-key" is not connected

Reconnect the plug before you continue:

$ workshop connect dev/secret-demo:api-key dev/system:demo-key

The workshopctl get-secret reference lists every exit status. Branch on the status when the SDK needs to distinguish an unavailable provider, a missing item, multiple matches, or an unconnected plug.

Every request also creates a Retrieve secret change. The change and its tasks record whether delivery succeeded and why it failed, but never contain the value. Use workshop changes and workshop tasks to troubleshoot an integration that hides the immediate error.

Use a tool-native credential helper

Prefer a tool-native credential helper when one is available. Configure the helper to run:

$ workshopctl get-secret <SDK>.<PLUG>

The tool controls when it calls the helper and how long it retains the returned value. The value does not need to enter the tool’s environment.

For example, Claude Code’s apiKeyHelper setting runs a command and uses its output as the API key.

If the tool accepts the credential on standard input instead, pipe the command output directly into the tool. Keep shell tracing such as set -x disabled, because it can print assignments and command substitutions.

Wrap a tool that requires an environment variable

When a tool reads its credential only from an environment variable, ship a wrapper that requests the value and exports it only for the tool process.

The following wrapper starts bin/secret-demo-tool with SECRET_DEMO_API_KEY. A value already supplied by the user takes precedence:

files/bin/secret-demo
#!/usr/bin/bash
# Runs secret-demo-tool with the API key from the connected secret plug.
# A key already set in the environment takes precedence.
sdk_dir=$(dirname "$(dirname "$(readlink -f "$0")")")

if [[ -z "${SECRET_DEMO_API_KEY:-}" ]]; then
  if key=$(workshopctl get-secret secret-demo.api-key); then
    export SECRET_DEMO_API_KEY="$key"
  fi
  unset key
fi

exec "$sdk_dir/bin/secret-demo-tool" "$@"

If the request fails, workshopctl get-secret already writes the diagnostic. The wrapper starts the tool without the variable, allowing the tool to use another login mechanism or report that the credential is missing.

If a silent fallback is intentional, redirect standard error only for the request:

if key=$(workshopctl get-secret secret-demo.api-key 2>/dev/null); then
  export SECRET_DEMO_API_KEY="$key"
fi

The corresponding Retrieve secret change remains available for troubleshooting while Workshop retains it.

Install the wrapper on PATH from the SDK’s setup-base hook:

hooks/setup-base
#!/usr/bin/bash
cat >/etc/profile.d/secret-demo.sh <<PROFILE
export PATH="${SDK}/bin:\$PATH"
PROFILE

Rebuild the SDK to include the wrapper and the hook:

$ sdkcraft try

Refresh the test workshop to install the rebuilt SDK:

$ workshop refresh

Then check that the tool receives the key:

$ workshop exec dev -- secret-demo

  secret-demo-tool: authenticated with a 23-character key

Also test the unavailable path by disconnecting the plug or locking the provider. The wrapper must not turn a failed lookup into a success-shaped result.

Deliver a secret to a systemd service

Use a systemd credential for a long-running service. The service reads the credential from the file named after it in CREDENTIALS_DIRECTORY.

For example, this service process rejects a missing or empty credential before starting its long-running work:

files/bin/secret-demo-service
#!/usr/bin/bash
# Reads the API key from a systemd credential, then runs the service.
key_file="$CREDENTIALS_DIRECTORY/secret-demo.api-key"
if [[ ! -s "$key_file" ]]; then
  echo "secret-demo-service: no API key in the secret-demo.api-key credential" >&2
  exit 1
fi
echo "secret-demo-service: started with a $(wc -c <"$key_file")-byte key"
exec sleep infinity

Install the unit from setup-base. Runtime hooks receive SDK_SYSTEMD_SECRET_SOCKET, which is the socket systemd uses to request the value:

hooks/setup-base
#!/usr/bin/bash
cat >/etc/profile.d/secret-demo.sh <<PROFILE
export PATH="${SDK}/bin:\$PATH"
PROFILE

cat >/etc/systemd/system/secret-demo.service <<UNIT
[Unit]
Description=Secret demo service

[Service]
User=workshop
LoadCredential=secret-demo.api-key:${SDK_SYSTEMD_SECRET_SOCKET}
ExecStart=${SDK}/bin/secret-demo-service
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target
UNIT

systemctl daemon-reload
systemctl enable --now secret-demo.service

Rebuild the SDK to include the service and the updated hook:

$ sdkcraft try

Refresh the test workshop to install the rebuilt SDK and start the service:

$ workshop refresh

Each service start causes systemd to request the value again. If the plug is unconnected or the lookup fails, systemd still starts the unit with an empty credential. Let the service reject that state, and configure a restart policy or restart it after fixing the lookup.

Expect an empty credential in two cases:

  • On initial launch, the service can start before the user connects the plug.

  • During a refresh, an updated SDK’s setup-base hook runs before Workshop restores preserved connections. The first service start can therefore receive an empty credential.

The refresh you just ran is the second case. To request the value immediately, whether after a refresh, after connecting the plug, or after correcting the lookup, restart the service:

$ workshop exec dev -- sudo systemctl restart secret-demo.service

Then check the service journal. The start during the refresh rejected the empty credential, and the restart received the key:

$ workshop exec dev -- sudo journalctl --no-pager -u secret-demo.service

  ...
  Oct 13 10:41:57 dev secret-demo-service[488]: secret-demo-service: no API key in the secret-demo.api-key credential
  ...
  Oct 13 10:42:01 dev secret-demo-service[522]: secret-demo-service: started with a 23-byte key

When delivery fails, inspect both the service journal and the workshop-secret@ journal:

$ workshop exec dev -- sudo journalctl --no-pager -u secret-demo.service -u 'workshop-secret@*'

The credential lives in a systemd-managed file for as long as the service runs. The unit’s user and root can read it; other processes running as the same user can also read the file. This is not process-level isolation.

Accept a secret for a single command

Users can also pass a credential ad hoc to a single workshop exec or workshop run invocation, without a slot or a connection. The value stays off the command line: users give the --env flag only the variable’s name and fill the variable from a credential store on the host with a command substitution:

$ SECRET_DEMO_API_KEY=$(secret-tool lookup service secret-demo account test) workshop exec --env SECRET_DEMO_API_KEY dev -- secret-demo

  secret-demo-tool: authenticated with a 23-character key

Accept the value in the variable that the wrapper exports for the tool, so that an ad hoc value overrides the connected plug: the wrapper above gives an existing value precedence and requests the connected secret only when the variable is absent. If the named variable is unset, Workshop skips it without an error, and the wrapper falls back to the plug.

Document the variable’s name for users, and tell them to pass only the name. A value typed into --env <NAME>=<VALUE> stays in the shell history, and other processes on the host can read it from the process list while the command runs.

Review the exposure limits

Each delivery mechanism keeps the value out of SDK content, but the value remains readable somewhere while it is in use:

Mechanism

What can read the value

What the SDK cannot clean up

Tool-native helper or standard input

The process that consumes the command output and anything to which that process passes it

While the plug is connected, any process in the workshop can make a new request

Command wrapper

The tool and every process it starts, through the tool’s environment

While the plug is connected, any process in the workshop can request the value with workshopctl get-secret

Systemd credential

The unit’s user, root, and other processes running as that user

The credential file remains available until systemd stops the service

Single-command value

The command and every process it starts

A variable exported by the user’s shell or by a tool such as direnv stays available to every command run from that environment

Disconnecting a plug prevents new requests, but it cannot withdraw a value that a process has already received. Document this limitation for users, especially when the SDK can run autonomous tools.

See also

How-to guides:

Reference: