SamRewritten › CLI

Using the SamRewritten CLI

A scriptable Steam Achievement Manager for your terminal. Same engine as the graphical app, no GTK required, and every command speaks JSON.

Getting the CLI

The CLI is a separate binary from the graphical app — it is not a flag you pass to the GUI. Grab it from the Releases page or install the Snap.

PlatformWhat to run
Linux (AppImage) Download samrewritten-cli.AppImage, then chmod +x samrewritten-cli.AppImage and run it.
Linux (Snap) samrewritten.samrewritten-cli. The Snap reuses the Steam folder you granted through the GUI, so run a GUI command once before using the CLI.
Windows samrewritten-cli.exe, in the installation folder or the portable ZIP. The installer deliberately creates no shortcut for it.
From source cargo build --release --no-default-features --features cli

The examples below call the binary samrewritten. Substitute whichever name applies to your install, or add an alias:

alias samrewritten=~/Downloads/samrewritten-cli.AppImage

Every command supports --help, and samrewritten --help lists them all.

How it behaves

  • Steam must be running and signed in. The CLI talks to your live Steam client; it does not log in on its own.
  • Results go to stdout as JSON, diagnostics go to stderr. That means you can pipe straight into jq and keep the noise on your terminal.
  • Exit status is 0 on success, non-zero on failure, so && and set -e work as you would expect.
  • You will briefly appear in-game. Any command that reads or changes a game's progress opens a short Steam session for that app, exactly as the graphical version does.
  • Commands are one-shot. Each invocation starts a backend process, does its work, and shuts down — idle being the one that stays alive on purpose.

Using this on multiplayer games is strongly discouraged. You alone are responsible for what happens to your Steam account; see the end-user agreement in the README.

Finding IDs

Everything else takes an App ID and, usually, achievement or stat API names — the internal identifiers, not the titles you see in the Steam overlay. These three commands are how you find them.

list-apps

samrewritten list-apps [--with-achievements] [--with-playtime]

Lists every app on your account.

--with-achievements
Adds achievement_count and unlocked_achievement_count. Slower: it has to query stats for every app you own.
--with-playtime
Adds playtime_minutes and last_played.
$ samrewritten list-apps --with-playtime --with-achievements
[
  {
    "app_id": 440,
    "app_name": "Team Fortress 2",
    "image_url": "https://.../capsule_231x87.jpg",
    "app_type": "App",
    "developer": "Valve",
    "metacritic_score": 92,
    "playtime_minutes": 338,
    "last_played": 1785521891,
    "achievement_count": 520,
    "unlocked_achievement_count": 0
  },
  ...
]

app_type is one of App, Mod, Demo or Junk — the last covers soundtracks, dedicated servers and other entries you probably want to filter out.

list-achievements

samrewritten list-achievements <APP_ID> [--language <LANGUAGE>]

Lists an app's achievements and whether you have each one.

$ samrewritten list-achievements 440
[
  {
    "id": "TF_SCOUT_KILL_IN_DODGE_COOLDOWN",
    "is_achieved": false,
    "unlock_time": null,
    "permission": 0,
    "icon_normal": "https://.../7acbb467.jpg",
    "icon_locked": "https://.../bf906416.jpg",
    "name": "Retire the Runner",
    "description": "Kill a Scout while they are under the effect of Crit-a-Cola.",
    "global_achieved_percent": 11.3
  },
  ...
]

id is the API name you pass to unlock and lock. A non-zero permission means Steam marks the achievement as protected and will refuse to change it. unlock_time is a Unix timestamp, or null if the achievement is still locked.

list-statistics

samrewritten list-statistics <APP_ID> [--language <LANGUAGE>]

Lists an app's statistics and their current values. Each entry is tagged Integer or Float, which is what decides how a new value is written.

$ samrewritten list-statistics 440
[
  {
    "Integer": {
      "id": "TF_PYRO_DEFEND_POINTS_STAT",
      "app_id": 440,
      "display_name": "TF_PYRO_DEFEND_POINTS_STAT",
      "is_increment_only": false,
      "permission": 0,
      "original_value": 0,
      "int_value": 0,
      "min_value": -2147483648,
      "max_value": 2147483647
    }
  },
  ...
]

Float stats have the same shape with float_value in place of int_value. Games often leave display_name equal to the API name, as Team Fortress 2 does here.

Locking and unlocking achievements

unlock

samrewritten unlock <APP_ID> <IDS>...

Unlocks one or more achievements, then saves. Prints one result object per ID.

$ samrewritten unlock 480 ACH_WIN_ONE_GAME ACH_WIN_100_GAMES
[
  { "id": "ACH_WIN_ONE_GAME", "success": true },
  { "id": "ACH_WIN_100_GAMES", "success": true }
]

lock

samrewritten lock <APP_ID> <IDS>...

The reverse: re-locks the listed achievements. Same output shape.

unlock-all

samrewritten unlock-all <APP_ID>

Unlocks every achievement the app defines.

$ samrewritten unlock-all 480
{"success":true}

lock-all

samrewritten lock-all <APP_ID>

Resets the app to a factory state.

This clears statistics as well as achievements, returning every stat to its default value. If you only meant to re-lock achievements, use lock with a list of IDs instead — and consider taking an export first.

Editing statistics

set-stat

samrewritten set-stat <APP_ID> <STAT_ID> <VALUE>

Writes a new value for one statistic. You do not have to say whether the stat is an integer or a float — the command looks it up in the game's schema and writes it accordingly, rejecting a value that does not fit.

$ samrewritten set-stat 440 TF_PYRO_DEFEND_POINTS_STAT 250
{"id":"TF_PYRO_DEFEND_POINTS_STAT","success":true}

$ samrewritten set-stat 440 TF_PYRO_DEFEND_POINTS_STAT 3.7
Stat TF_PYRO_DEFEND_POINTS_STAT takes an integer: invalid digit found in string

Stats that Steam marks as protected are refused, the same way the graphical version greys them out. Note that set-stat does not second-guess a game's is_increment_only flag the way import does: it asks Steam for the write and reports what came back, so lowering such a stat may simply not take.

Reading names in another language

Achievement titles, descriptions and stat display names come from a schema Steam caches locally, and that file usually ships every language the game was translated into. By default you get the game's own language; --language picks a different one.

list-languages

samrewritten list-languages <APP_ID>

Shows what that particular game offers. This one reads the cached schema directly, so it works without Steam running and without starting the game.

$ samrewritten list-languages 440
[
  "LATAM",
  "brazilian",
  "bulgarian",
  ...
  "tchinese",
  "thai",
  "turkish",
  "ukrainian"
]

$ for lang in english french japanese; do
    samrewritten list-achievements 440 --language $lang \
      | jq -r --arg l "$lang" '.[] | select(.id == "TF_HEAVY_DAMAGE_TAKEN") | "\($l): \(.name)"'
  done
english: Iron Kurtain
french: Rideau de fer
japanese: 鉄のカーテン

Matching ignores case, so --language FRENCH works too. A name the game does not ship is rejected outright rather than quietly falling back, and the error lists what is actually available:

$ samrewritten list-achievements 440 --language klingon
App 440 has no 'klingon' in its schema. Available: LATAM, brazilian, bulgarian, ...

--language only affects display text. IDs never change, so a script that filters on id behaves identically in any language. Exports are always written in the game's own language, which keeps exported files comparable no matter who produced them.

Idling apps

idle

samrewritten idle <APP_ID>

Makes you appear in-game until you stop it. Unlike every other command this one stays in the foreground; press Ctrl+C (or send SIGTERM) and it shuts the session down cleanly.

$ samrewritten idle 440
Idling app 440. Press Ctrl+C to stop.

Import and export

Snapshots use the same JSON format as the graphical version, so a file taken in one can be restored by the other.

export

samrewritten export <APP_IDS>...

Writes a snapshot of one or more apps to stdout. Progress is reported on stderr, so redirecting stdout gives you a clean file.

$ samrewritten export 440 480 > backup.json
Exported 1/2
Exported 2/2

$ head -13 backup.json
{
  "format_version": 1,
  "exported_at": "2026-07-31T18:18:25Z",
  "apps": [
    {
      "app_id": 480,
      "app_name": "",
      "achievements": [
        {
          "id": "ACH_WIN_ONE_GAME",
          "is_achieved": true,
          "permission": 0
        },
        ...

import

samrewritten import <FILE> [--app-id <APP_ID>]

Restores a snapshot. Pass --app-id to apply just one app out of a file covering several. Protected achievements and stats are skipped rather than failing the run, and the summary names them.

$ samrewritten import backup.json --app-id 480
Imported 1/1
[
  {
    "app_id": 480,
    "achievements_applied": 7,
    "stats_applied": 0,
    "skipped_protected": [],
    "skipped_unwriteable": [],
    "errors": [],
    "reset_would_help": false
  }
]

reset_would_help is a hint for one specific case: the snapshot wanted to lower a stat the game only ever lets increase. Running lock-all first, then importing again, gets you there — which is why it is only set when nothing else in the run failed outright.

Scripting recipes

All of these use jq. Diagnostics land on stderr, so nothing needs filtering out of the pipe.

List what you are still missing in a game

samrewritten list-achievements 440 \
  | jq -r '.[] | select(.is_achieved | not) | "\(.id)\t\(.name)"'

Unlock only the achievements most players already have

A gentler alternative to unlock-all: everything earned by more than half of all players.

samrewritten unlock 440 $(
  samrewritten list-achievements 440 \
    | jq -r '.[] | select(.global_achieved_percent > 50) | .id'
)

Find the games you own that have achievements

samrewritten list-apps --with-achievements \
  | jq -r '.[] | select(.app_type == "App" and .achievement_count > 0)
           | "\(.app_id)\t\(.unlocked_achievement_count)/\(.achievement_count)\t\(.app_name)"'

Back up a handful of games before experimenting

samrewritten export 440 480 570 > backup-$(date +%F).json

You can feed the whole library in the same way, but bear in mind each app is opened in turn, so a large account takes a while.

Environment variables

Rarely needed, but useful when detection guesses wrong or you keep several Steam installs.

VariableEffect
SAM_STEAM_INSTALL_ROOT Linux only. Use this Steam installation instead of the detected one — handy when a Snap, Flatpak and native Steam coexist on the same machine.
SAM_STEAMCLIENT_PATH Linux only. Load this specific steamclient.so.
SAM_APP_LIST_URL Fetch the app list from a different URL.
SAM_STEAM_INSTALL_ROOT=~/snap/steam/common/.local/share/Steam samrewritten list-apps

Something not behaving as documented? Please open an issue.