Skip to content

JVM SDK

Version compatibility: Available since SecretSpec 0.20.

The JVM SDK (org.cachix.secretspec-jvm) is a thin client over the same Rust resolver as the CLI. Every provider, fallback chain, profile, generator, reference, and as_path secret therefore works without JVM-side resolution logic.

In your build.gradle.kts dependencies:

implementation("org.cachix:secretspec-jvm:$secretspecVersion")

In your pom.xml dependencies:

<dependency>
<groupId>org.cachix</groupId>
<artifactId>secretspec-jvm</artifactId>
<version>${secretspec.version}</version>
</dependency>

The package targets JDK 11 and includes native resolvers for glibc and musl Linux x64/Arm64, macOS x64/Arm64, and Windows x64/Arm64. Windows assets statically include the C runtime. No separate SecretSpec CLI, native library, Visual C++ Redistributable, or system libdbus installation is needed.

package org.cachix.examples;

import org.cachix.secretspec.SecretSpec;

public class QuickStartExample {

    public static void main(String[] args) {
        try (var resolved = SecretSpec.builder()
            .withProvider("keyring://")
            .withProfile("production")
            .withReason("boot web app")
            .load()
        ) {
            System.out.println(resolved.provider() + " (" + resolved.profile() + ")");
            System.out.println(resolved.secret("DATABASE_URL").get());
            resolved.setAsSystemProperties();
        }
    }
}

get() returns the inline value, or the readable file path for an as_path secret. A missing required secret throws MissingRequiredException; its missing() method returns the secret names. Other failures throw SecretSpecException, whose kind() method returns a stable error category.

Use withScope("api") to resolve only a named [scopes.api] subset. The selected name is available as Resolved.scope() and ResolutionReport.scope():

package org.cachix.examples;

import org.cachix.secretspec.SecretSpec;

public class ScopeExample {

    public static void main(String[] args) {
        try (var resolved = SecretSpec.builder().withScope("api").load()) {
            resolved.setAsSystemProperties();
        }
    }
}

report() returns the inventory view exposed by secretspec check --json. It never carries values. Missing required secrets appear with status().equals("missing_required") rather than throwing, so incomplete deployments can still be inspected.

package org.cachix.examples;

import org.cachix.secretspec.SecretSpec;

public class ReportExample {

    public static void main(String[] args) {
        var report = SecretSpec.builder()
            .withProfile("production")
            .withReason("deployment preflight")
            .report();

        for (var secret : report.secrets())
            System.out.println(secret.name() + ": " + secret.status());
    }
}

Generate an idiomatic language model from the manifest schema:

Terminal window
secretspec schema |
quicktype -s schema --top-level AppSecrets --lang java -o AppSecrets.java

Then deserialize the SDK’s flat field map:

package org.cachix.examples;

import org.cachix.secretspec.SecretSpec;
import io.quicktype.AppSecrets;
import io.quicktype.Converter;

import java.io.IOException;

public class TypedAccessExample {

    public static void main(String[] args) throws IOException {
        try (var resolved = SecretSpec.builder().load()) {
            AppSecrets typed = Converter.fromJsonString(resolved.fieldsJson());
            System.out.println(typed.getDatabaseURL());
        }
    }
}

The schema models successful resolution: required, defaulted, and generated secrets are non-nullable, and profile-specific schemas include inherited default-profile fields.

File-shaped secrets are materialized as mode-0400 temporary files. The returned path must remain valid after load(), so the caller owns its lifetime. Resolved implements AutoCloseable; use a try-with-resources declaration or call close() to remove these files deterministically:

package org.cachix.examples;

import org.cachix.secretspec.SecretSpec;

public class AsPathExample {

    public static void main(String[] args) {
        try (var resolved = SecretSpec.builder().withReason("TLS boot").load()) {
            var secrets = resolved.secrets();
            var certificatePath = secrets.get("TLS_CERT").get();
            // Use the certificate before resolved is disposed.
            System.out.println(certificatePath);
        }
    }
}

Caller context answers what invoked SecretSpec (for example, git). It is deliberately separate from the user-supplied access reason, which answers why the access is happening and may be required by a project’s require_reason policy. Caller context never satisfies that policy.

The context is caller-asserted metadata, not an authenticated identity. It is included in audit events and forwarded to providers that choose to consume it. Do not put credentials or secret values in any field.

package org.cachix.examples;

import org.cachix.secretspec.SecretSpec;
import org.cachix.secretspec.Caller;

public class CallerExample {

    public static void main(String[] args) {
        try (var resolved = SecretSpec.builder()
            .withProvider("keyring://")
            .withProfile("production")
            .withCaller(Caller.named("caller name")
                .withVersion("optional caller version")
                .withOperation("optional operation")
                .withResource("optional resource")
            )
            .withReason("boot web app")
            .load()
        ) {
            System.out.println(resolved.provider() + " (" + resolved.profile() + ")");
            System.out.println(resolved.secret("DATABASE_URL").get());
        }
    }
}

The Jar runtime asset is selected automatically. For local SDK development, SECRETSPEC_FFI_LIB can point to a particular libsecretspec_ffi build. From a SecretSpec source checkout, the SDK also searches an ancestor Cargo target/debug or target/release directory.