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.
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.
| Value | path field in leaf | Use case |
|---|---|---|
"null" | null | Prove 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 |
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.
include_no_did = trueSetting 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.
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_fileFor 500 files, 2 DIDs, 3 path variants, and include_no_did = true:
leaves_per_file = 3 × (2 + 1) = 9
total_leaves = 500 × 9 = 45004500 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:
{
"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 bytesdatetime: file mtime in RFC 3339 UTC (time.RFC3339, second precision)did: DID string or JSONnullpath: path string or JSONnull
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.
| Priority | Source | If the file is missing |
|---|---|---|
| 1 | --config PATH | Fatal error, the run stops |
| 2 | SIMPLE_OTS_CONFIG_PATH env var | Fatal error, the run stops |
| 3 | <out-dir>/config.toml, when --out is used | Fall through to 4 |
| 4 | <cwd>/.simple-ots/config.toml | Built-in defaults |
A path you asked for explicitly (1 and 2) must exist. Typos are reported instead of being silently replaced by the defaults:
$ simple-ots --config ./nope.toml
error loading config: config file not found: ./nope.tomlEvery run prints the file it actually used, so you never have to guess:
Config: /home/you/project/.simple-ots/config.toml
Root hash: 9967626790d4e313aeca255c9d9183e8308a8d88823ccefed62de29f72b9ef75When 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:
git ls-files | simple-ots --out ./.personal/.simple-ots
# → ./.personal/.simple-ots/results/YYYYMMDD_HHMMSS/| Priority | Source |
|---|---|
| 1 | --out DIR, alias --output-dir DIR |
| 2 | SIMPLE_OTS_OUT_DIR env var |
| 3 | output_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/.
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_204949The 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
# .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.