Skip to content

Austrum-lab/riftwrap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

riftwrap

A launch wrapper: injects environment variables, prefix commands and Vault secrets into a command from layered TOML profiles, then replaces itself with the target via exec. Zero extra processes, zero runtime overhead.

Two use cases, one engine:

  • Linux gaming (the primary one): wrap Steam's %command%, toggle MangoHud/GameMode/gamescope/vkBASALT per game, per group of games, or per Proton version.

    Steam → riftwrap → [umu-run] → Proton/Wine → game
    
  • Secrets-injecting launcher: run any command with env and Vault secrets from a named profile — see Outside gaming.

riftwrap decides whether to enable tools like MangoHud and with which flags/env — it does not replace their own configs, and it does not manage Proton versions (that's umu/Lutris territory).

Installation

Download the latest release for your platform (Linux/macOS, amd64/arm64 are detected automatically) and put it into PATH:

curl -fLo riftwrap "https://github.com/Austrum-lab/riftwrap/releases/latest/download/riftwrap-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')"
sudo install -m755 riftwrap /usr/local/bin/
riftwrap --version

(any directory in PATH works; /usr/local/bin is the conventional place for manually installed binaries). The result is a single fully static binary with no runtime dependencies. On macOS a binary downloaded by a browser gets the quarantine attribute (curl downloads don't) — if Gatekeeper complains, run xattr -d com.apple.quarantine riftwrap before installing. Note that on macOS only the secrets-injecting use case applies — the gaming toggles wrap Linux tools.

Building from source

CGO_ENABLED=0 go build -trimpath \
  -ldflags "-s -w -X github.com/Austrum-lab/riftwrap/cmd.version=$(git describe --tags 2>/dev/null || echo dev)" \
  -o riftwrap .
sudo install -m755 riftwrap /usr/local/bin/

CGO_ENABLED=0 keeps the net package off libc, so the binary runs on any distro. Development build: go build; tests: go test ./.... Cross-compiling needs no extra toolchain:

GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o riftwrap .

Shell completion (bash; zsh/fish work the same way):

riftwrap completion bash | sudo tee /usr/share/bash-completion/completions/riftwrap

Releases are built by CI: pushing a v* tag triggers .github/workflows/release.yml, which tests, cross-builds all platforms with the version stamped in, and attaches the binaries to a GitHub Release.

Quick start

  1. Put riftwrap somewhere in PATH.

  2. In Steam: game → Properties → Launch Options:

    riftwrap %command%
    

    (a -- separator before %command% is accepted but not required)

  3. Create a profile for the game (AppID is in the store page URL):

    riftwrap profile new cyberpunk2077 --appid=1091500
    riftwrap profile edit cyberpunk2077
  4. Check what will happen without launching anything:

    riftwrap resolve --appid 1091500

First run creates ~/.config/riftwrap/ with a commented riftwrap.toml.

How profiles are matched

Configuration lives in ~/.config/riftwrap/:

riftwrap.toml     # main config = base profile (always applied first) + includes
conf.d/*.toml     # one file = one profile, name = file name without .toml

riftwrap.toml holds base [env]/[wrap.*] settings and the include globs (relative to the config dir):

include = ["conf.d/*.toml"]

What a profile matches is decided by its [match] section, not by file location. All matchers present in one profile must match together (AND), and the profile merges at the level of its strongest matched kind — higher level wins on key conflict:

1. riftwrap.toml           (base, always)
2. community sources       (below everything local)
3. group                   (profiles pulled in by name via match.group)
4. match.regex             (regex on the full command line)
5. match.exe               (glob on the executable name, case-insensitive)
6. match.deck              (Steam Deck detection)
7. match.appid             (SteamAppId / SteamGameId / GAMEID env)
8. --profile / RIFTWRAP_PROFILE

A profile with no matchers applies only when referenced — by name via --profile, or via another profile's group list (that's how shared "group" presets work: group = ["dx11-heavy"] pulls in conf.d/dx11-heavy.toml at level 3, transitively and cycle-safe). An explicitly selected profile expands its group list too, and each profile applies once, at the strongest level it earned.

--only skips layers 1–7 and applies just the explicit profile — no group expansion. [meta].priority orders multiple matches within one level (higher applies later, i.e. wins). wrap.extra_prefix.commands concatenates across layers instead of overwriting; set [meta].replace_extra_prefix = true to reset the accumulated list from that layer on.

The executable is detected by walking the launch argv from the end: the first argument ending in .exe (case-insensitive) is the Windows binary; if there is none, the last argument is taken as a native binary.

A broken profile (bad TOML, invalid regex/glob) never blocks a launch: wrap mode prints a warning to stderr and skips it. doctor and resolve report the same problems as errors.

Profile reference

Every key is optional; a profile can be a single [env] block.

[match]                      # all present matchers must match (AND)
appid = 1091500              # Steam AppID
exe   = "Cyberpunk2077.exe"  # glob on the executable basename
regex = "proton.*experimental" # regex on the full command line
deck  = true                 # true = Steam Deck only, false = off-deck only
group = ["dx11-heavy"]       # profiles (by name) to pull in when this matches
                             # ("groups" is an accepted alias for this key)

[env]                        # plain env vars, higher layers win per key;
                             # bare numbers/bools are fine (true -> "1"),
                             # but text values must be quoted (TOML syntax);
                             # a leading "~/" expands to the home directory
                             # (nothing downstream expands tildes — a literal
                             # "~/x" would become a relative path)
DXVK_HUD = "fps,memory"
PROTON_ENABLE_NVAPI = 1

[secrets]                    # Vault KV v2 refs "<path>/<field>[@<mount>]"
                             # (GitLab CI style); resolved right before exec,
                             # never written to disk
DISCORD_RPC_TOKEN = "riftwrap/discord/token@secret"

[wrap.gamemode]
enabled = true               # prefixes gamemoderun
args    = []                 # optional extra gamemoderun arguments

[wrap.mangohud]
enabled = true               # prefixes mangohud
config  = "~/.config/MangoHud/custom.conf"  # sets MANGOHUD_CONFIGFILE
args    = ["--dlsym"]

[wrap.gamescope]
enabled   = true             # prefixes gamescope -W .. -H .. -r .. <args> --
width     = 1920
height    = 1080
fps_limit = 60
args      = ["--hdr-enabled"] # optional extra gamescope flags

[wrap.vkbasalt]
enabled = true               # sets ENABLE_VKBASALT=1

[wrap.obs_vkcapture]
enabled = true               # sets OBS_VKCAPTURE=1

[wrap.prime_offload]
enabled = true               # sets __NV_PRIME_RENDER_OFFLOAD / DRI_PRIME etc.

[wrap.cpu]
taskset = "0-7"              # prefixes taskset -c 0-7 (no privileges needed)
nice    = -5                 # prefixes nice -n -5; negative values need a
                             # raised limit, see "About negative nice" below

[wrap.wine_sync]
esync = true                 # WINEESYNC=1; false sets WINEESYNC=0 + PROTON_NO_ESYNC=1
fsync = true                 # same for fsync

[wrap.extra_prefix]          # escape hatch: arbitrary prefix commands
commands = [["some-tool", "--flag"]]

[meta]
priority  = 0                # merge order within one layer
mask_keys = ["MY_TOKEN"]     # extra [env] keys to mask in resolve/log output

Prefix command order (outermost first): nice, taskset, gamescope --, gamemoderun, mangohud, extra_prefix commands, then the original command.

About negative nice

Raising priority (negative nice) is not allowed for regular users by default: nice prints "Permission denied" and the game still starts, just at normal priority. To make it apply, raise the allowed ceiling in /etc/security/limits.conf and re-login:

<your-user>  -  nice  -5

This does not change the default priority of anything — processes still start at nice 0; it only permits going down to -5 when a profile asks for it. (Alternatively, GameMode's daemon renices games via polkit without any limit changes.)

Example: a real setup

Three layers that compose well in practice — global defaults, Steam-wide tweaks, and per-game settings:

# riftwrap.toml — applies to every wrapped launch
include = ["conf.d/*.toml"]

[wrap.mangohud]
enabled = true
# conf.d/steam-any.toml — every Steam launch contains steam-launch-wrapper
# in its command line, so this regex hits all Steam games (level 4)
[match]
regex = "steam-launch-wrapper"

[env]
PROTON_USE_NTSYNC = 1
PROTON_ENABLE_WAYLAND = 1
VKD3D_CONFIG = "descriptor_heap"
PULSE_LATENCY_MSEC = 50
# conf.d/helldivers2.toml — one game (level 7, overrides the layers above
# on key conflicts)
[match]
appid = 553850

[env]
LOW_LATENCY_LAYER = 1
LOW_LATENCY_LAYER_REFLEX = 1

Bare numbers and booleans are fine in [env]; text values must be quoted (TOML syntax). Proton-only variables in the Steam-wide layer are harmless for native Linux games — they just ignore them.

Commands

riftwrap [--profile=<name>] [--only] [--] %command%   wrap mode (used by Steam)

riftwrap resolve [--appid=<id>] [--exe=<name>] [--cmd="<line>"]
                                  show matched layers, final env and command
riftwrap profile list             list the base config and included profiles
riftwrap profile new <name> [--appid=<id>] [--exe=<glob>] [--regex=<re>] [--deck] [--group=<a,b>]
                                  matcher flags combine (AND)
riftwrap profile edit <name>      open in $EDITOR, validate on save
riftwrap profile show <name>      print normalized TOML
riftwrap doctor                   check tools in PATH, validate all profiles
                                  (TOML + regex/glob), ping Vault if configured
riftwrap log [-n N]               show recent launches from the pre-launch log
riftwrap source add <git-url> [--name=<n>] [--path=<dir>] [--allow-unsafe]
riftwrap source update [<name>]   fetch, show diff, confirm, re-pin
riftwrap source remove <name>
riftwrap source list
riftwrap completion bash|zsh|fish shell completion

resolve --cmd accepts shell-style quoting, so paths with spaces work:

riftwrap resolve --cmd '"/data/Steam/steamapps/common/My Game/game.exe" -skiplauncher'

Secrets (HashiCorp Vault)

[secrets] values are KV v2 references in GitLab CI style — <path>/<field>[@<mount>], e.g. riftwrap/discord/token@secret. The mount defaults to secret and the KV v2 data/ API segment is inserted automatically. References are resolved lazily right before exec and never cached to disk; each distinct Vault path is fetched once per launch, so several fields off one path cost a single request.

References may contain $VAR / ${VAR}, expanded from riftwrap's own environment first, then from the merged [env] of local profile layers — never from community profiles, which must not be able to steer where a secret is read from. An undefined variable aborts the launch:

[secrets]
DB_PASSWORD = "${VAULT_BASE}/myapp/db/password@my-kv"

Auth, from the environment:

  • VAULT_ADDR + VAULT_TOKEN, or
  • VAULT_ADDR + VAULT_ROLE_ID + VAULT_SECRET_ID (AppRole; mount defaults to approle, override with VAULT_APPROLE_MOUNT). An explicit token wins.

If Vault is unreachable or a path/field is missing, the launch is aborted — the game never starts half-configured. Everything that came from [secrets] is always shown as *** in resolve output and in the log; use [meta].mask_keys to mask ordinary [env] keys too.

riftwrap only ever reads from Vault (one AppRole login per launch, then one GET per reference) — grant its token or AppRole read capability on the referenced paths and nothing else. With AppRole, each launch issues a fresh token that is never revoked (the wrapper execs away), so give the role a short token_ttl if you launch often.

Profile sources (git)

riftwrap source add https://github.com/someone/gaming-profiles
riftwrap source update          # shows a diff, asks for confirmation

A source repository ships its profiles as riftwrap.conf.d/*.toml (another directory can be chosen with --path when adding the source). The source name defaults to the repository path from the URL (someone/gaming-profiles above), and profile names get a <source>/ prefix: someone/gaming-profiles/dx11-heavy. Reference them from a local profile's group list — --profile only looks at local profiles.

Community sources are sandboxed by design:

  • synced to ~/.local/share/riftwrap/community/, physically separate from local profiles;
  • pinned to a commit — source update shows the diff and re-pins only after confirmation;
  • merged below all local profiles, so anything local overrides them;
  • cannot define wrap.extra_prefix (arbitrary commands) unless the source was added with --allow-unsafe;
  • never feed ${VAR} expansion in secret references.

See docs/community-sources.md for a full walkthrough: naming, referencing, private repos, update workflow.

Launch log

Every launch appends one JSON line to ~/.local/share/riftwrap/log/riftwrap.log before exec: timestamp, matched layers, applied env (secrets masked), final argv. The log self-rotates (last 50 launches or 14 days). riftwrap log pretty-prints it. Exit codes are not tracked — the wrapper is gone once the game starts.

Other launchers (Lutris, Heroic)

Profile resolution is not tied to Steam: the exe/runtime/group layers work off argv alone, so riftwrap can wrap launches from Lutris/Heroic as well — only the appid/ layer needs Steam's env vars.

Outside gaming: secrets-injecting launcher

An explicit-only profile (no matchers) plus --only turns riftwrap into a lightweight vault agent/envconsul-style wrapper for any command:

# ~/.config/riftwrap/conf.d/db-prod.toml
[env]
PGHOST = "prod-db.internal"

[secrets]
PGPASSWORD = "team/db/password@secret"
riftwrap --profile db-prod --only -- psql -U app mydb

--only skips the whole gaming pipeline (the riftwrap.toml base and every matched profile); the secret is fetched right before exec, never touches disk, and shows up as *** in resolve and the launch log.

License

MIT — see LICENSE.

About

Per-process env & wrapper-tool injection for Linux via layered TOML profiles. Steam/Proton-friendly, Vault secrets included

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages