Scrippy

The Python scripting framework for efficient sysadmins

Write production-grade admin scripts with arguments, configuration, logging, execution history, access and concurrency control built in. Coded by sysadmins for sysadmins.

Read the documentation

Why Scrippy?

Simplify scripting, maintain focus, and uphold quality, reliability and efficiency

Scrippy streamlines scripting with standardized formats, essential features like configuration settings and argument parsing, logging and secrets management, access and concurrent execution control.

Simplifying scripting while maintaining high standards allows for more effective goal achievement by system administrators and developers.

Scrippy is fully open-source

Browse and patch the code, submit pull requests to our repositories at Codeberg.

Focus on the Task

Argument parsing, configuration checks, logging and locking are handled by Scrippy: your script only contains what it is meant to do

Consistent Scripts Across Teams

Every Scrippy script follows the same layout and offers the same options (--help, --hist, --log...): anyone in the team can read, run and maintain it

Know What Happened

Every execution gets its own log and a line in the script history: who ran it, with which parameters, how long it took and how it ended

Secrets Stay Secret

Passwords and tokens declared as secrets are redacted from log files, console output and execution history

Safe in Production

Only authorized users and groups run your scripts, concurrent executions are kept under control and errors end with a clear message and an exit code, not a stack trace

It's Just Python

No new language to learn: the Scrippy header is a plain docstring and your script remains a regular Python file, free to use any Python library

Batteries for Daily Operations

Official modules for SSH/SFTP, FTP and CIFS, databases, REST APIs, mail, Git, SNMP and templates, all sharing Scrippy's conventions

Free / Open Source

Scrippy is released under the MIT license, so you can spend your money on other stuff

Stay focused on the task

Stop wasting your time trying to remember how to handle arguments or getting lost in the execution log management system

Stay focused on the task you want to accomplish and write beautiful, reliable, and standardized scripts with which you can review detailed execution history, fine-tune execution permissions, manage concurrent executions and more.

Increase maintainability

Increase maintainability and ensure consistency across all scripts within your teams by adhering to Scrippy's formalism.

Discover the core features

Scrippy Core Features

Everything an admin script needs, built in

Declarative script header for metadata, arguments, configuration and policies
Command line options parsed, validated and documented in --help
Configuration files checked against declared sections, keys and types
Secrets redacted from log files, console output and execution history
Per-execution log and history with user, parameters, duration, exit code and error
Execution restricted to authorized users and groups
Concurrent executions limited, queued or refused
Temporary workspace created and cleaned up for each execution

See it in action: a real-life admin task

Every night, the rotated logs of the billing servers must be archived and their disk space freed. Only members of the ops group may run the job, never twice at the same time, and a remote file is deleted only once its archived copy has been verified.

#!/bin/env python3
"""
--------------------------------------------------------------------------------
  @author         : Harry Fink
  @date           : 2026-10-07
  @version        : 1.0.0
  @description    : Archive rotated application logs and free remote disk space

--------------------------------------------------------------------------------
  Update:
  1.0.0  2026-10-07 - Harry Fink - cre: Production release

--------------------------------------------------------------------------------
  List of authorized users or group:
    @group:ops

--------------------------------------------------------------------------------
  Concurrent executions:
    @max_instance: 1
    @exit_on_wait: true

--------------------------------------------------------------------------------
  List of mandatory configuration parameters:
    @conf:ssh|user|str|false
    @conf:ssh|port|int|false
    @conf:ssh|key|str|false
    @conf:logs|remote_dir|str|false
    @conf:logs|pattern|str|false
    @conf:archive|dir|str|false

--------------------------------------------------------------------------------
  List of execution options and argument parameters:
    @args:hosts|str|Servers to collect the logs from|+||||true|false
    @args:dry-run|bool|List the files to archive without touching them||false|||false|false

--------------------------------------------------------------------------------
  Functioning:
  ---------------
    For each server, rotated log files are downloaded to the archive
    directory, verified against their SHA-256 checksum, and only then
    deleted from the server.
"""
#-------------------------------------------------------------------------------
#  Initialization of the environment
#-------------------------------------------------------------------------------
import os
import shlex
import hashlib
import datetime
import scrippy_core
from scrippy_core import logger
from scrippy_remote.ssh import Ssh

#-------------------------------------------------------------------------------
#  Definition of functions and classes
#-------------------------------------------------------------------------------
class ChecksumError(Exception):
  pass

def sha256(path):
  with open(path, "rb") as archived:
    return hashlib.sha256(archived.read()).hexdigest()

def archive_logs(host, config, dry_run):
  logger.info(f"[+] Archiving logs from {host}")
  remote_dir = config.get("logs", "remote_dir")
  archive_dir = os.path.join(config.get("archive", "dir"), host,
                             datetime.date.today().isoformat())
  os.makedirs(archive_dir, exist_ok=True)
  with Ssh(username=config.get("ssh", "user"),
           host=host,
           port=config.get("ssh", "port", "int"),
           key=config.get("ssh", "key")) as ssh:
    names = ssh.list_remote_dir(remote_dir, pattern=config.get("logs", "pattern"))
    logger.info(f" '-> {len(names)} rotated log file(s) found")
    for name in names:
      remote_file = f"{remote_dir}/{name}"
      if dry_run:
        logger.info(f" '-> Would archive: {remote_file}")
        continue
      result = ssh.exec(f"sha256sum {shlex.quote(remote_file)}")
      checksum = result["stdout"][0].split()[0]
      ssh.get_file(remote_file, archive_dir)
      if sha256(os.path.join(archive_dir, name)) != checksum:
        raise ChecksumError(f"{host}:{remote_file}")
      ssh.delete_remote_file(remote_file)
      logger.info(f" '-> Archived: {remote_file}")

#-------------------------------------------------------------------------------
#  Main processing
#-------------------------------------------------------------------------------
def main():
  with scrippy_core.ScriptContext() as _context:
    for host in _context.args.hosts:
      archive_logs(host, _context.config, _context.args.dry_run)

#-------------------------------------------------------------------------------
#  Entry point
#-------------------------------------------------------------------------------
if __name__ == '__main__':
  main()

The script configuration file, exp_archive_logs.conf, is checked against the @conf declarations before anything runs:

[ssh]
  user = svc-logs
  port = 22
  key = /home/harry.fink/.ssh/id_ed25519
[logs]
  remote_dir = /var/log/billing
  pattern = .*\.log\.\d+\.gz$
[archive]
  dir = /srv/archive/billing

Try it first, then let cron run it every night:

exp_archive_logs.py --hosts bill01 bill02 --dry-run
exp_archive_logs.py --hosts bill01 bill02

The next morning, everyone in the team knows what happened:

$ exp_archive_logs.py --hist 3
3 last executions of exp_archive_logs.py
+--------------------------+---------------------+---------------------+--------------+-----------------+-----------------+------+----------------------------------------------+---------------+
|         Session          |        Start        |         End         |   Duration   |      Origin     |       Exec      | Code |                    Params                    |      Exit     |
+--------------------------+---------------------+---------------------+--------------+-----------------+-----------------+------+----------------------------------------------+---------------+
| 1791385201.4127718_48213 | 08/10/2026 02:00:01 | 08/10/2026 02:03:12 | 3 min 11 sec |    harry.fink   |    harry.fink   |  0   |       ['--hosts', 'bill01', 'bill02']        |       0       |
| 1791298801.2084511_31877 | 07/10/2026 02:00:01 | 07/10/2026 02:01:46 | 1 min 45 sec |    harry.fink   |    harry.fink   |  1   |       ['--hosts', 'bill01', 'bill02']        | ChecksumError |
| 1791289240.9361205_30154 | 06/10/2026 23:20:40 | 06/10/2026 23:20:42 |    2 sec     | luiggi.vercotti | luiggi.vercotti |  0   | ['--hosts', 'bill01', 'bill02', '--dry-run'] |       0       |
+--------------------------+---------------------+---------------------+--------------+-----------------+-----------------+------+----------------------------------------------+---------------+

What went wrong on October 7th? The full log of each execution is one --log <SESSION> away. The remote file was kept, ready for the next run:

$ exp_archive_logs.py --log 1791298801.2084511_31877
[20261007 02:00:01] [INFO    ] [+] Opening history (retention=50)
[20261007 02:00:01] [INFO    ] [+] Using workspace: /var/tmp/scrippy/exp_archive_logs_1791298801.2084511_31877
[20261007 02:00:01] [INFO    ] [+] Archiving logs from bill01
[20261007 02:00:02] [INFO    ]  '-> 3 rotated log file(s) found
[20261007 02:00:31] [INFO    ]  '-> Archived: /var/log/billing/billing.log.1.gz
[20261007 02:00:58] [INFO    ]  '-> Archived: /var/log/billing/billing.log.2.gz
[20261007 02:01:19] [INFO    ]  '-> Archived: /var/log/billing/billing.log.3.gz
[20261007 02:01:19] [INFO    ] [+] Archiving logs from bill02
[20261007 02:01:20] [INFO    ]  '-> 2 rotated log file(s) found
[20261007 02:01:45] [INFO    ]  '-> Archived: /var/log/billing/billing.log.1.gz
[20261007 02:01:46] [INFO    ] [+] Workspace deletion: /var/tmp/scrippy/exp_archive_logs_1791298801.2084511_31877
[20261007 02:01:46] [INFO    ] [+] End: 1 (ChecksumError)
[20261007 02:01:46] [CRITICAL] [ChecksumError]: bill02:/var/log/billing/billing.log.2.gz

Install Scrippy and boost your efficiency

Scrippy installation is no harder than any other Python module and running a command:

pip install scrippy
scrippy install

That's it, you are ready to leap forward!

In the system Python environment

On recent operating systems (Debian 12+, Ubuntu 23.04+...), the system Python environment is externally managed by the operating system (PEP 668). Add the --break-system-packages option to install Scrippy alongside the Python packages provided by your distribution:

pip install --break-system-packages scrippy
scrippy install --break-system-packages

This method is best suited to hosts where you keep control over the system Python packages.

In a virtual environment

A dedicated virtual environment keeps Scrippy and its dependencies isolated from the operating system and works on every system:

python3 -m venv ~/.local/share/scrippy/venv
~/.local/share/scrippy/venv/bin/pip install scrippy
~/.local/share/scrippy/venv/bin/scrippy install

Run your scripts with the ~/.local/share/scrippy/venv/bin/python interpreter. For a system-wide installation, run the same commands as root with /opt/scrippy/venv as the virtual environment.

Write your first Scrippy Python script