Skip to content

Adding an Integration

An integration lets a tool that is not SecretSpec read its secrets from a SecretSpec provider, instead of keeping a second copy in that tool’s own store. Git credentials is the first one SecretSpec ships.

There is no single way to build one. Which mechanism fits depends on how the tool takes input, whether you can change its source, and whether it needs a value once at startup or repeatedly while it runs. Pick from the table, then read the matching section.

MechanismUse whenYou writeShips in
Environment injectionThe tool reads environment variables or a file pathNothingAvailable today
An SDKYou control the tool’s source and can link a libraryCalls into the SDKAvailable today
IPCA separate program needs on-demand resolution, prompting, or write-back, and must not link SecretSpecA protocol client0.20+
An in-tree integrationThe tool speaks its own credential or helper protocol, and the integration should ship with SecretSpecA module and a shim binary0.20+

Work down the list. Environment injection costs nothing and covers most tools, so reach past it only when something concrete rules it out: the tool needs a value it did not have at startup, it must not hold the value in its environment for its whole lifetime, or it has a credential protocol of its own that expects to be asked.

The shortest path. secretspec run resolves the profile and executes the tool with the values in its environment:

Terminal window
$ secretspec run -- terraform apply

Narrow what the tool receives with a scope, so a single manifest can serve several tools without handing each one everything:

Terminal window
$ secretspec run --scope deploy -- terraform apply

For a tool that wants a file rather than a variable, declare the secret with as_path = true and it receives a path to a resolver-owned temporary file.

This mechanism resolves once, before the tool starts. It cannot re-resolve a rotated value, ask a person for input mid-run, or write a value back. When one of those matters, keep reading.

If you own the tool’s source, link SecretSpec directly rather than wrapping it. The SDK resolves on demand inside the process, with no subprocess and no protocol to implement. See the SDK overview for the available languages and how each one ships.

This is the right answer for your own applications. It is the wrong answer for a third-party tool you do not control, and for any consumer that must not take on SecretSpec’s dependency closure.

IPC covers the case the other two cannot: a separate, long-lived program that resolves names while it runs, without linking SecretSpec or any provider SDK.

The consumer launches secretspec serve as a child process and speaks the Secret Resolution Protocol over its standard input and output. A session is bound at initialization to one manifest, provider, profile, scope, and access reason, and from then on resolves one exact declared name at a time. That bound scope is what makes IPC safe to hand to a consumer you would not hand a whole profile.

Beyond resolution, a session can:

  • return a resolver-owned file path under an explicit lease, for a value that should not pass through an environment variable;
  • ask the launching process for a value through the client.prompt callback, since the resolver has no terminal of its own;
  • store or remove one declared name with resolver.set and resolver.delete, for a tool such as cargo login that authenticates and then wants to keep the result.

Optional callbacks and mutations are advertised as capabilities, so a client can tell an older endpoint apart from one that refused a particular request. Launch with secretspec serve --read-only when the consumer must never cause a write.

You have three ways to write the client:

ClientLanguageNotes
secretspec-ipcRustAsync by default; its blocking feature gives a synchronous session for a program with no async runtime
libsecretspec-resolverC11Portable source plus static and shared libraries, with no Rust in its dependency closure
Your ownAnyImplement the wire protocol directly

Read the IPC architecture first for the trust boundaries, then the protocol pages for the contract.

Some tools already define how they ask for a credential. Git invokes a helper binary and exchanges attributes with it on stdin and stdout; other tools have their own equivalents. For those, the integration belongs inside SecretSpec, so a user installs one thing and configures it with secretspec.

Integrations live in secretspec/src/integration/. Read integration/git.rs alongside this section: it is the worked example, and it is the only one so far.

An integration must not depend on the current working directory. A user running git push from any directory expects the same credential, so the integration carries its own manifest rather than discovering one:

const EMBEDDED_MANIFEST: &str = include_str!("git-credentials.toml");

Keep it small. The Git manifest declares exactly two secrets, a required PASSWORD and an optional USERNAME, and sets require_reason = false because the tool invoking the helper cannot supply a reason.

One embedded manifest usually has to serve many targets: several Git hosts, several accounts on one host. Derive an identity from the canonical attributes of the target and use it to isolate values.

Rewrite the project name and suffix the key names:

config.project.name = format!("git-credential-{identity}");
let password_secret = format!("{EMBEDDED_PASSWORD}_{identity}");

Both are necessary. Some providers flatten a convention address to the logical key and ignore project and profile entirely, so an identity carried only in the project name would collapse every target onto one value in those stores.

Canonicalize before hashing, so that equivalent spellings of the same target select the same credential and genuinely different targets never collide.

Register the binary the tool will invoke in secretspec/Cargo.toml:

[[bin]]
name = "git-credential-secretspec"
path = "src/bin/git-credential-secretspec.rs"
required-features = ["cli"]

Keep it a thin entry point that delegates into the integration module, and match the primary CLI’s behavior for the environment it runs in. The Git shim restores default SIGPIPE handling so it exits quietly when its output pipe closes.

The shim serves the tool. A secretspec <tool> subcommand group serves the person, and should cover the whole lifecycle:

Terminal window
$ secretspec git configure --url https://github.com --username YOUR_USERNAME
$ secretspec git login https://github.com
$ secretspec git logout https://github.com
$ secretspec git unconfigure --url https://github.com

Where the integration writes into the tool’s own configuration, treat that file as shared. Register alongside existing entries rather than replacing them, write durably, preserve symlinks, and leave recoverable state when removal fails partway.

Add docs/src/content/docs/integrations/<tool>.md, then add it to the Integrations group in docs/astro.config.ts. State what the integration does not cover: the Git page says up front that it does not manage SSH keys or inject secrets into repositories, which saves a reader from discovering that later.

Mark every new command, field, and page with its target version, as described in the release visibility checklist.