Skip to content

Configuration

Configuration is read from .simple-ots/config.toml in the current directory, or from the path given to --config.

The file is TOML format. All fields are optional; if the file doesn't exist, defaults are used.

Fields

dids

Type []string, default [] (empty).

A list of DID strings to bind to each file's leaf hashes. Each DID produces a separate leaf per file per path variant. This is the core mechanism for selective disclosure of identity: you can reveal a file proof tied to one DID without disclosing that the same file is also tied to another identity.

toml
dids = ["did:web:alice.example.com", "did:web:bob.example.com"]

DIDs are arbitrary strings, so any string that is unique and meaningful to you works. The did:web:, did:key:, and did:ion: methods are common choices, but there is no validation beyond being a non-empty string.

path_variants

Type []string, default ["null", "filename", "relative"]. Only those three values are valid.

Controls which representations of the file path are included in each leaf. Each variant produces a separate leaf, enabling selective disclosure of path information.

Valuepath field in leafUse case
"null"nullProve file content and DID without revealing its location
"filename""main.go"Reveal the filename but not the directory structure
"relative""src/main.go"Reveal the full relative path from the project root
toml
path_variants = ["null", "filename", "relative"]

include_no_did

Type bool, default true.

When true, an additional leaf is generated for each file with did = null, regardless of the dids list. This enables proving that a file existed without revealing any identity.

toml
include_no_did = true

Setting this to false requires at least one DID in dids; otherwise no leaves would be generated for any file.

output_dir

Type string, default "" (meaning .simple-ots/).

Where results/ is written. See Output directory below.

toml
output_dir = "./.personal/.simple-ots"

Leaf count formula

leaves_per_file = len(path_variants) × (len(dids) + (include_no_did ? 1 : 0))
total_leaves    = files × leaves_per_file

For 500 files, 2 DIDs, 3 path variants, and include_no_did = true:

leaves_per_file = 3 × (2 + 1) = 9
total_leaves    = 500 × 9     = 4500

4500 SHA-256 hashes = ~144 KB of leaf data. Merkle tree depth ≈ 13 levels. OTS anchor = 1 root hash (32 bytes). Negligible overhead.

Canonical JSON structure

The leaf hash is SHA-256(canonical_JSON) where canonical JSON is produced by marshalling a map[string]interface{} with Go's encoding/json, which sorts keys lexicographically. The resulting JSON always has this shape:

json
{
  "content_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "datetime": "2026-06-26T12:00:00Z",
  "did": "did:web:example.com",
  "path": "src/main.go"
}
  • content_sha256: hex SHA-256 of raw file bytes
  • datetime: file mtime in RFC 3339 UTC (time.RFC3339, second precision)
  • did: DID string or JSON null
  • path: path string or JSON null

Keys are always in alphabetical order (content_sha256, datetime, did, path). This makes the hash fully reproducible on any platform with any JSON library, as long as null values are included (not omitted).

Config file location

Sources are tried in this order; the first one that applies wins.

PrioritySourceIf the file is missing
1--config PATHFatal error, the run stops
2SIMPLE_OTS_CONFIG_PATH env varFatal error, the run stops
3<out-dir>/config.toml, when --out is usedFall through to 4
4<cwd>/.simple-ots/config.tomlBuilt-in defaults

A path you asked for explicitly (1 and 2) must exist. Typos are reported instead of being silently replaced by the defaults:

console
$ simple-ots --config ./nope.toml
error loading config: config file not found: ./nope.toml

Every run prints the file it actually used, so you never have to guess:

Config:    /home/you/project/.simple-ots/config.toml
Root hash: 9967626790d4e313aeca255c9d9183e8308a8d88823ccefed62de29f72b9ef75

When no file is found at all, that line reads (defaults, no config file found).

Output directory

By default everything is written under .simple-ots/ in the current directory. Change it with output_dir, --out, or SIMPLE_OTS_OUT_DIR:

bash
git ls-files | simple-ots --out ./.personal/.simple-ots
# → ./.personal/.simple-ots/results/YYYYMMDD_HHMMSS/
PrioritySource
1--out DIR, alias --output-dir DIR
2SIMPLE_OTS_OUT_DIR env var
3output_dir in the config file that was loaded
4.simple-ots/

Relative paths are resolved against the current working directory; the location of the config file has no bearing on them. That is what makes one shared config usable everywhere: with output_dir = "./.personal/.simple-ots" in a config you point at from another machine or another drive, each project's results land under that project's own .personal/.

console
PS C:\Users\tmp\my-project> $env:SIMPLE_OTS_CONFIG_PATH = "C:\pg2\.simple-ots\config.toml"
PS C:\Users\tmp\my-project> simple-ots
Config:    C:\pg2\.simple-ots\config.toml
Output:    C:\Users\tmp\my-project\.personal\.simple-ots\results\20260727_204949

The directory is created if it does not exist. In walk mode it is skipped, so a run never hashes its own results. Where the output lands has no effect on leaf hashes or the Merkle root.

One subtlety: a config file discovered inside a custom output directory can set DIDs and path variants, but its own output_dir is ignored, since the directory had to be known already to find that file. A config named with --config or SIMPLE_OTS_CONFIG_PATH has no such limitation.

Full example

toml
# .simple-ots/config.toml

# Bind hashes to these identities
dids = [
  "did:web:alice.example.com",
  "did:web:bob.example.com",
]

# Path representations to generate per file
path_variants = ["null", "filename", "relative"]

# Always include a leaf with no DID (for anonymous proofs)
include_no_did = true

# Keep results outside the repo root (optional)
output_dir = "./.personal/.simple-ots"

See Config Examples for more patterns.

Released under the Apache License 2.0.