A zero-dependency Go library that generates unique, deterministic machine identifiers from hardware characteristics. IDs are stable across reboots, sensitive to hardware changes, and ideal for software licensing, device fingerprinting, and telemetry correlation.
- Zero Dependencies — built entirely on the Go standard library
- Cross-Platform — macOS, Linux, and Windows
- Configurable — choose which hardware signals to include (CPU, Motherboard, System UUID, MAC, Disk)
- Power-of-2 Output — 32, 64, 128, or 256 hex characters
- SHA-256 Hashing — cryptographically secure, no collisions in practice
- Salt Support — application-specific IDs on the same machine
- VM Friendly — preset for virtual environments (CPU + UUID)
- Thread-Safe — safe for concurrent use after configuration
- Diagnostic API — inspect which components succeeded or failed
- Optional Logging —
*slog.Loggersupport for observability with zero overhead when disabled - Structured Errors — sentinel errors and typed errors for programmatic handling via
errors.Is/errors.As - Testable — dependency-injectable command executor
Add the module to your Go project:
go get github.com/slashdevops/machineidRequires Go 1.25+. No external dependencies.
go install github.com/slashdevops/machineid/cmd/machineid@latestMake sure ~/go/bin is in your PATH:
mkdir -p ~/go/bin
# bash
cat >> ~/.bash_profile <<EOL
export PATH=\$PATH:~/go/bin
EOL
source ~/.bash_profile
# zsh
cat >> ~/.zshrc <<EOL
export PATH=\$PATH:~/go/bin
EOL
source ~/.zshrcPrecompiled binaries for macOS, Linux, and Windows are available on the releases page.
You can download them with the GitHub CLI (gh):
brew install gh # if not already installedThen fetch and install the binary:
export TOOL_NAME="machineid"
export GIT_ORG="slashdevops"
export GIT_REPO="machineid"
export OS=$(uname -s | tr '[:upper:]' '[:lower:]')
export OS_ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
export ASSETS_NAME=$(gh release view --repo ${GIT_ORG}/${GIT_REPO} --json assets -q "[.assets[] | select(.name | contains(\"${TOOL_NAME}\") and contains(\"${OS}\") and contains(\"${OS_ARCH}\"))] | sort_by(.createdAt) | last.name")
gh release download --repo $GIT_ORG/$GIT_REPO --pattern $ASSETS_NAME
unzip $ASSETS_NAME
rm $ASSETS_NAME
mv $TOOL_NAME ~/go/bin/$TOOL_NAME
~/go/bin/$TOOL_NAME -versionClone the repository and build with version metadata via the provided Makefile:
git clone https://github.com/slashdevops/machineid.git
cd machineid
make build
./build/machineid -versionpackage main
import (
"context"
"fmt"
"log"
"github.com/slashdevops/machineid"
)
func main() {
ctx := context.Background()
id, err := machineid.New().
WithCPU().
WithSystemUUID().
ID(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(id)
// Output: 64-character hex string (e.g. b5c42832542981af…)
}Enable one or more hardware sources via the With* methods:
ctx := context.Background()
provider := machineid.New().
WithCPU(). // processor ID and feature flags
WithMotherboard(). // motherboard serial number
WithSystemUUID(). // BIOS/UEFI system UUID
WithMAC(). // physical network interface MAC addresses (default filter)
WithDisk() // internal disk serial numbers
id, err := provider.ID(ctx)Control which network interfaces are included in the machine ID using MACFilter:
ctx := context.Background()
// Physical interfaces only (default, most stable for bare-metal)
id, _ := machineid.New().WithCPU().WithMAC().ID(ctx)
// All interfaces including virtual (VPN, Docker, bridges)
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterAll).ID(ctx)
// Only virtual interfaces (useful for container-specific fingerprinting)
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterVirtual).ID(ctx)| Filter | Interfaces Included | Best For |
|---|---|---|
MACFilterPhysical |
en0, eth0, wlan0 (default) |
Bare-metal stability |
MACFilterAll |
Physical + virtual (docker0, utun, bridge, etc.) |
Maximum uniqueness |
MACFilterVirtual |
docker0, utun, bridge0, veth, vmnet, etc. |
Container fingerprinting |
All formats produce pure hexadecimal strings without dashes:
ctx := context.Background()
// 32 characters (2^5) — compact
id, _ := machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format32).ID(ctx)
// 64 characters (2^6) — default, full SHA-256
id, _ = machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format64).ID(ctx)
// 128 characters (2^7) — extended
id, _ = machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format128).ID(ctx)
// 256 characters (2^8) — maximum
id, _ = machineid.New().WithCPU().WithSystemUUID().WithFormat(machineid.Format256).ID(ctx)| Format | Length | Bits | Collision Probability (1 B IDs) | Use Case |
|---|---|---|---|---|
Format32 |
32 | 128 | ~1.47 × 10⁻²¹ | Compact identifiers |
Format64 |
64 | 256 | ~4.32 × 10⁻⁶⁰ | Default, recommended |
Format128 |
128 | 512 | Virtually zero | Extended security |
Format256 |
256 | 1024 | Astronomically low | Maximum security |
A salt ensures the same machine produces different IDs for different applications:
ctx := context.Background()
id, _ := machineid.New().
WithCPU().
WithSystemUUID().
WithSalt("my-app-v1").
ID(ctx)For virtual machines where disk serials and MACs may be unstable:
ctx := context.Background()
id, _ := machineid.New().
VMFriendly(). // CPU + System UUID only
WithSalt("my-app").
ID(ctx)Check whether a stored ID still matches the current hardware:
ctx := context.Background()
provider := machineid.New().WithCPU().WithSystemUUID()
valid, err := provider.Validate(ctx, storedID)Inspect which hardware components were successfully collected:
ctx := context.Background()
provider := machineid.New().
WithCPU().
WithSystemUUID().
WithDisk()
id, _ := provider.ID(ctx)
diag := provider.Diagnostics()
fmt.Println("Collected:", diag.Collected) // e.g. [cpu uuid]
fmt.Println("Errors:", diag.Errors) // e.g. map[disk: no internal disk identifiers found]Enable optional logging with any *slog.Logger for observability. When no logger is set (the default), there is zero overhead:
import (
"log/slog"
"os"
)
ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
id, err := machineid.New().
WithCPU().
WithSystemUUID().
WithLogger(logger).
ID(ctx)| Log Level | What's Logged |
|---|---|
| Info | Component collected, fallback triggered, ID generation lifecycle |
| Warn | Component failed or returned empty value |
| Debug | Command execution details (name, args, duration), raw hardware values |
The logger is compatible with slog.Default() which bridges to the standard log package:
// Use the standard library default logger
provider.WithLogger(slog.Default())The package provides sentinel errors for errors.Is and typed errors for errors.As:
id, err := provider.ID(ctx)
if errors.Is(err, machineid.ErrNoIdentifiers) {
// No hardware identifiers were collected
}| Error | Meaning |
|---|---|
ErrNoIdentifiers |
No hardware identifiers collected with current config |
ErrEmptyValue |
A component returned an empty value |
ErrNoValues |
A multi-value component (MAC, disk) returned no values |
ErrNotFound |
A value was not found in command output or system files |
ErrOEMPlaceholder |
A value matches a BIOS/UEFI placeholder ("To be filled...") |
ErrAllMethodsFailed |
All collection methods for a component were exhausted |
Use errors.As to extract structured context from errors:
// Check if a system command failed
var cmdErr *machineid.CommandError
if errors.As(err, &cmdErr) {
fmt.Println("command:", cmdErr.Command) // e.g. "sysctl", "ioreg", "wmic"
}
// Check if output parsing failed
var parseErr *machineid.ParseError
if errors.As(err, &parseErr) {
fmt.Println("source:", parseErr.Source) // e.g. "system_profiler JSON"
}
// Inspect diagnostic errors per component
diag := provider.Diagnostics()
var compErr *machineid.ComponentError
if errors.As(diag.Errors["cpu"], &compErr) {
fmt.Println("component:", compErr.Component)
fmt.Println("cause:", compErr.Err)
}A ready-to-use command-line tool is included.
See the Installation section above for all ways to install the CLI.
# Generate an ID from CPU + UUID (default 64 chars)
machineid -cpu -uuid
# All hardware sources, compact 32-char format
machineid -all -format 32
# VM-friendly with custom salt
machineid -vm -salt "my-app"
# JSON output with diagnostics
machineid -cpu -uuid -json -diagnostics
# Validate a previously stored ID
machineid -cpu -uuid -validate "b5c42832542981af58c9dc3bc241219e780ff7d276cfad05fac222846edb84f7"
# Info-level logging (fallbacks, lifecycle events)
machineid -cpu -uuid -verbose
# Include only physical MACs (default)
machineid -mac -mac-filter physical
# Include all MACs (physical + virtual)
machineid -all -mac-filter all
# Debug-level logging (command details, raw values, timing)
machineid -all -debug
# Version information
machineid -version
machineid -version.long| Flag | Description |
|---|---|
-cpu |
Include CPU identifier |
-motherboard |
Include motherboard serial number |
-uuid |
Include system UUID |
-mac |
Include network MAC addresses |
-mac-filter F |
MAC filter: physical (default), all, or virtual |
-disk |
Include disk serial numbers |
-all |
Include all hardware identifiers |
-vm |
VM-friendly mode (CPU + UUID only) |
-format N |
Output length: 32, 64 (default), 128, or 256 |
-salt STRING |
Custom salt for application-specific IDs |
-validate ID |
Validate an ID against the current machine |
-diagnostics |
Show collected/failed components |
-json |
Output as JSON |
-verbose |
Enable info-level logging to stderr (fallbacks, lifecycle) |
-debug |
Enable debug-level logging to stderr (commands, values, timing) |
-version |
Show version information |
-version.long |
Show detailed version information |
- Collect — gather hardware identifiers based on the provider configuration
- Sort — sort identifiers alphabetically for deterministic ordering
- Hash — apply SHA-256 to the concatenated identifiers (with optional salt)
- Format — truncate or extend the hash to the selected power-of-2 length
| Platform | CPU | UUID | Motherboard | Disk | MAC |
|---|---|---|---|---|---|
| macOS | sysctl, system_profiler |
system_profiler, ioreg |
system_profiler, ioreg |
system_profiler |
net.Interfaces |
| Linux | /proc/cpuinfo |
/sys/class/dmi/id, /etc/machine-id |
/sys/class/dmi/id |
lsblk, /sys/block |
net.Interfaces |
| Windows | wmic, PowerShell |
wmic, PowerShell |
wmic, PowerShell |
wmic, PowerShell |
net.Interfaces |
Each source has fallback methods for resilience across OS versions and configurations.
The library supports dependency injection for deterministic testing without real system commands:
type mockExecutor struct {
outputs map[string]string
}
func (m *mockExecutor) Execute(ctx context.Context, name string, args ...string) (string, error) {
if output, ok := m.outputs[name]; ok {
return output, nil
}
return "", fmt.Errorf("command not found: %s", name)
}
provider := machineid.New().
WithExecutor(&mockExecutor{
outputs: map[string]string{
"sysctl": "Intel Core i9",
},
}).
WithCPU()
id, err := provider.ID()Run the test suite:
go test -v -race ./...- SHA-256 is a cryptographically secure one-way hash — hardware details cannot be recovered from an ID
- Sorting ensures consistent output regardless of collection order
- Salt support prevents cross-application ID reuse
- No personally identifiable information (PII) is exposed in the output
| Format | Recommendation |
|---|---|
Format32 |
Embedded systems or storage-constrained environments |
Format64 |
Recommended for most use cases (default) |
Format128 |
Extra security margin or regulatory requirements |
Format256 |
Maximum security for critical applications |
// Minimal (VMs, containers)
id, _ := machineid.New().VMFriendly().ID()
// Balanced (recommended)
id, _ := machineid.New().
WithCPU().
WithSystemUUID().
WithMotherboard().
ID()
// Maximum (most unique, but sensitive to hardware changes)
id, _ := machineid.New().
WithCPU().
WithSystemUUID().
WithMotherboard().
WithMAC().
WithDisk().
ID()If you encounter this error when trying to push a tag:
! [remote rejected] v0.0.1 -> v0.0.1 (push declined due to repository rule violations)
error: failed to push some refs to 'github.com:slashdevops/machineid.git'
Cause: This happens when you try to create a tag with a version number that is older than existing tags. GitHub repository rules enforce semantic versioning order to prevent version rollback.
Solution: Create a tag with a version number higher than all existing tags.
-
Check existing tags:
git tag -l
-
Create the next appropriate version:
# If the latest tag is v0.0.2, use v0.0.3 or higher git tag -a "v0.0.3" -m "Release v0.0.3" git push origin v0.0.3
For more information about versioning and releases, see CONTRIBUTING.md.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines on how to contribute, including information about versioning, testing, and code style.