Contributing to ProxMenux

About~15 min

ProxMenux is open to community contributions — new scripts, fixes for existing ones, dialog improvements, translations, integrations. This page is the formal door: how the repository is structured, the branching model and pull-request workflow, the script header with author attribution and optional sponsor link, and the design conventions every contribution must follow.

Two related pages, one project

Don't confuse this page with Contributors: that one celebrates the people who already contribute (testers, reviewers, developers). This page is for you — the prospective contributor — and explains how to send a PR that will be merged.

Branching model

ProxMenux uses a three-tier branching model:

BranchPurpose
mainStable, production-ready code. Only release-grade merges land here. This is what end users get when they install ProxMenux normally.
developActive development branch. All new work lands here first; it's the “beta” channel. Stable releases are cut from here into main when a release is ready.
feature/*Short-lived branches per feature, fix or improvement. Created from develop, merged back into develop via a Pull Request after review.

What this means for users

End users tracking main get tested releases. Users who want the latest features early — and don't mind occasional rough edges — can follow develop. New features always pass through develop first; nothing reaches main without going through that cycle.

Pull request workflow

From idea to merged release in five steps:

  1. Create a feature branch from develop:
    git clone https://github.com/MacRimi/ProxMenux.git
    cd ProxMenux
    git checkout develop
    git pull origin develop
    git checkout -b feature/your-feature-name
    Use a descriptive branch name: feature/zfs-arc-tuning, feature/fix-iommu-detection, feature/add-tailscale-script.
  2. Work on your changes and push the branch:
    # Make your changes, then:
    git add scripts/...
    git commit -m "Add ZFS ARC tuning script"
    git push -u origin feature/your-feature-name
  3. Open a Pull Request against develop (not against main). On the GitHub PR creation page, double-check the base branch is set to develop. Include in the description: what the script does, what it changes, and which Proxmox VE versions you tested it on.
  4. After review, changes are merged into develop. A maintainer reviews for code quality, header conventions, and the two-phase UI policy. They may request changes — push more commits to the same branch and the PR updates automatically.
  5. Stable releases are merged from develop into main. When the maintainers cut a release, develop gets fast-forwarded into main and tagged. Your contribution becomes part of the next stable version.

Script header — metadata & description

Every script in ProxMenux opens with two adjacent comment blocks that together form the header. They are both required — together they let any reader know who wrote the script and what it does, all without opening the code itself.

  • Top block — metadata. Author, optional GitHub / Sponsor links, maintainer, copyright, license, version, last-updated date. This is also where contributor recognition happens: when you write a new script, your name goes here, and you can optionally include a link to your personal page (GitHub) and a sponsor profile (Ko-fi, GitHub Sponsors, Buy Me a Coffee, etc.).
  • Bottom block — description. A short paragraph in plain English explaining what the script does. This is what users read before opening the code — it must be self-contained enough that someone who only sees the header understands the purpose of the script. List the main actions, the resources affected, any prerequisites.

The license line is fixed — GPL-3.0

ProxMenux is published under the GNU General Public License v3.0. Every script in the project ships under that same license; the License line in the header is always the GPL-3.0 reference shown in the example below — it's not a per-script choice. By contributing a script you agree to release it under GPL-3.0, which means anyone can read it, modify it and redistribute it (including modifications) as long as they keep it under the same license. The full text lives at MacRimi/ProxMenux/LICENSE.
#!/bin/bash

# ==========================================================
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author      : Your Name
# GitHub      : github.com/yourhandle
# Sponsor     : ko-fi.com/yourhandle
# Maintainer  : MacRimi
# Copyright   : (c) 2026 MacRimi & contributors
# License     : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version     : 1.0
# Last Updated: DD/MM/YYYY
# ==========================================================
# Description:
# Short paragraph explaining what the script does.
# Mention the main actions (e.g. "creates a ZFS pool",
# "configures IOMMU and reboots", "imports an ISO into a VM"),
# the resources it touches, and any prerequisites the user
# should be aware of before running it.
# ==========================================================

The GitHub and Sponsor lines are optional — leave them out if you don't want to publish them. Everything else is required.

Why this matters

Open-source contribution is voluntary work. Putting the contributor's name and (when provided) sponsor link directly in the source gives the people who build the scripts visible recognition every time someone reads the code — your authorship is preserved, not hidden under “the project”. The description block matters for a different reason: it's the first thing a future maintainer or curious user reads, and a clear description is often the difference between a script people trust and one they avoid.

Project structure

Where each kind of script lives in the repository:

scripts/
├── menus/              # Top-level menu scripts (entry points)
├── storage/            # Disk, storage and passthrough scripts
├── share/              # NFS, Samba, local share scripts
├── vm/                 # VM creation and configuration scripts
├── gpu_tpu/            # GPU / TPU passthrough scripts
├── post_install/       # Post-install automation scripts
├── backup_restore/     # Backup and restore scripts
├── utilities/          # System utility scripts
├── global/             # Shared helper libraries (sourced by other scripts)
├── utils.sh            # Shared utility functions and message helpers
└── help_info_menu.sh   # Interactive help and command reference

Every script sources utils.sh at startup to get the message functions, the spinner, color variables and the translation system. Shared helper libraries (in scripts/global/) are sourced explicitly by the scripts that need them.

The two-phase UI design policy

This is the most important convention in ProxMenux and the one PRs are most often asked to fix. Every script is divided into exactly two phases:

PhasePurposeScreen state
Phase 1 — SelectionCollect every user decision. Run any preparatory work needed (probes, scans, checks).dialog overlays. If a probe between two menus takes more than a second or two, show a msg_info spinner so the user knows the script hasn't frozen, then call stop_spinner right before the next dialog.
Phase 2 — ExecutionExecute every operation. Display the full progress history accumulating on screen.Visible msg_info / msg_ok messages; never dialog.

The principle: collect everything first, then execute everything. The user sees a clean dialog-driven menu (Phase 1), then a clean log-style execution view (Phase 2). No mixing — no dialog appearing mid-execution, no progress noise during selection.

Phase 1 — typical pattern

# Silent preparatory work between dialogs
msg_info "$(translate "Checking disk assignments...")"
ASSIGNED_TO=$(check_assignments "$DISK")
stop_spinner   # ← clears line silently, result saved in variable

# Next dialog can now use ASSIGNED_TO
if [ -n "$ASSIGNED_TO" ]; then
    dialog --yesno "$(translate "Disk already assigned. Continue?")" $UI_YESNO_H $UI_YESNO_W
fi

# Collect multiple decisions per item with parallel arrays
declare -a DISK_LIST=()
declare -a DISK_FORMAT_TYPES=()
declare -a DISK_MOUNT_POINTS=()
for DISK in $SELECTED; do
    msg_info "$(translate "Analyzing disk...")"
    CURRENT_FS=$(lsblk -no FSTYPE "$DISK" | xargs)
    stop_spinner

    FORMAT=$(dialog --backtitle "$BACKTITLE" \
        --title "$(translate "Select Filesystem")" \
        --menu "..." $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
        "ext4" "..." "xfs" "..." "btrfs" "..." \
        2>&1 >/dev/tty)
    [ -z "$FORMAT" ] && continue

    DISK_LIST+=("$DISK")
    DISK_FORMAT_TYPES+=("$FORMAT")
done

Rules for Phase 1:

  • If a msg_info spinner is currently running and you need to open a dialog or whiptail menu, call stop_spinner first — the spinner can't coexist with the overlay drawn by either tool. If no spinner is active, you don't need to call it.
  • Use show_proxmenux_logo + msg_title + msg_info when you need to give the user visual context for a long-running operation in Phase 1 (e.g. a probe that takes 5+ seconds). The function includes a screen clear, so don't call clear before it.
  • Don't call show_proxmenux_logo between dialog menus where there's nothing to display — clearing the screen for an empty terminal is just visual noise.
  • Store all decisions and probe results in variables or parallel arrays. The visible recap happens at the start of Phase 2, not in Phase 1.

Phase 2 — typical pattern

# ── PHASE 2 — EXECUTION ─────────────────────────────
show_proxmenux_logo
msg_title "$(translate "My Script Title")"

# Recap Phase 1 preparatory results — show what was already done
msg_ok "$(translate "CT $CTID selected.")"
msg_ok "$(translate "Repositories verified.")"
msg_ok "$(translate "Disks to process: ${#DISK_LIST[@]}")"

# Now execute operations
for i in "${!DISK_LIST[@]}"; do
    DISK="${DISK_LIST[$i]}"
    FORMAT="${DISK_FORMAT_TYPES[$i]}"

    msg_info "$(translate "Formatting") $DISK $(translate "as") $FORMAT..."
    mkfs."$FORMAT" "$DISK" >/dev/null 2>&1
    msg_ok "$(translate "Formatted.")"
done

msg_ok "$(translate "Completed. ${#DISK_LIST[@]} disk(s) processed.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r

Rules for Phase 2: always start with show_proxmenux_logo + msg_title; immediately recap Phase 1 results as msg_ok lines; never call show_proxmenux_logo again (it would clear accumulated progress); never call dialog in Phase 2 — if a runtime decision is truly unavoidable, use whiptail (see next section).

When to use dialog vs whiptail

Both tools draw text-mode user interfaces, but they behave very differently on screen — and ProxMenux uses them in two distinct phases for a reason.

ToolWhen to use itEffect on screen
dialogAlways in Phase 1. Every interactive selection — picking a VM, a disk, a filesystem, confirming an action — uses dialog. This is the default UI tool of ProxMenux.Takes over the terminal: clears the screen, draws its overlay, and on close returns the terminal to its prior state. Fine in Phase 1 because nothing useful is showing yet.
whiptailOnly in Phase 2, only when unavoidable. The typical case is a reboot prompt at the end of execution. Don't reach for whiptail to ask “continue?” mid-execution — that decision should have been made in Phase 1.Lighter-weight overlay that does not clear the terminal context. The accumulated msg_ok / msg_info history of Phase 2 stays visible behind the dialog box. That's why it's the right choice when progress is already on screen.

Why the split matters

If you call dialog in the middle of Phase 2, every msg_ok line the user has been watching disappears. You wipe the audit trail. whiptail avoids that. So the rule isn't arbitrary — it's about preserving the user's view of what the script has done so far.

Reboot prompt — the canonical Phase 2 whiptail

When a script ends and a reboot may be required (e.g. IOMMU enabled, kernel parameters changed), the prompt at the end of Phase 2 uses whiptail. Always include a “No” branch that warns the user not to use the affected resource until they reboot:

if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
  echo ""
  if whiptail --title "$(translate "Reboot Required")" --yesno \
    "\n$(translate "A host reboot is required before starting the VM. Reboot now?")" \
    13 78; then
      msg_warn "$(translate "Rebooting the system...")"
      reboot
  else
      echo ""
      msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
  fi
fi

msg_success "$(translate "Press Enter to return to menu...")"
read -r

Message functions reference

All defined in utils.sh. Use them as the default for any user-visible output — consistent visuals across scripts is the whole point. If your script needs a new function that doesn't fit the existing set (a new severity level, a new layout helper, etc.), propose it in your Pull Request — it'll be reviewed and added to utils.sh if it's broadly useful.

FunctionWhen to useSpinner
msg_info "text"Operation in progress.Starts
stop_spinnerEnd of silent preparatory work in Phase 1 — kills spinner, clears the line.Stops
msg_ok "text"Operation succeeded. Also use for “feature enabled” even when a reboot is required.Stops
msg_warn "text"Actual warning or degraded state.Stops
msg_error "text"Fatal error.Stops
msg_info2 "text"Non-blocking advisory (cyan info line).Stops
msg_success "text"Final “Press Enter to return” prompt at the end of Phase 2.Stops
msg_title "text"Bold title with built-in spacing. Used at the start of Phase 2.
show_proxmenux_logoClears screen, shows the logo. Called once at the start of Phase 2 only.

dialog conventions

  • Always pass --backtitle "$BACKTITLE" to every dialog and whiptail call. $BACKTITLE is always "ProxMenux" — set once at the script header and never overridden. The user must always see the project name as the framing context, never the script's own title.
  • Always wrap titles and messages with $(translate "...").
  • Always redirect dialog output with 2>&1 >/dev/tty — the captured stdout becomes the user's selection, while the dialog itself draws on the terminal.
  • Use the standard UI dimension variables ($UI_MENU_H, $UI_MSG_W, etc.) for consistent sizing across scripts.
  • Always check for empty / cancelled selections and handle them gracefully.

Complete example — building a VM-selection menu and handling cancellation:

# 1) Build the list of VMs as alternating "ID  NAME" pairs.
#    dialog --menu expects this exact shape: tag1 description1 tag2 description2 ...
VM_LIST=""
for vmid in $(qm list | awk 'NR>1 {print $1}'); do
    vm_name=$(qm config "$vmid" | awk -F': ' '/^name:/ {print $2}')
    VM_LIST="$VM_LIST $vmid \"$vm_name\""
done

# 2) Show the dialog. Captured stdout is the user's selection (the VMID).
VMID=$(eval "dialog --backtitle \"\$BACKTITLE\" \
    --title \"\$(translate \"Select VM\")\" \
    --menu \"\$(translate \"Choose a VM from the list:\")\" \
    \$UI_MENU_H \$UI_MENU_W \$UI_MENU_LIST_H \
    $VM_LIST \
    2>&1 >/dev/tty")

# 3) Empty result means the user pressed Cancel or Esc — exit silently.
if [ -z "$VMID" ]; then
    exit 0
fi

# 4) Continue with the rest of Phase 1, knowing VMID is now set.
echo "User selected VMID=$VMID"

Translation policy

All user-visible strings must be wrapped with the translate function. ProxMenux translates them automatically into all supported languages — you write English, the user reads their native language.

msg_ok "$(translate "Operation completed successfully.")"
msg_error "$(translate "Failed to start container") $CTID."
dialog --title "$(translate "Select Storage")" ...
  • Write strings in English — translation is handled automatically by the build.
  • Keep strings concise. Avoid embedding variables inside long sentences where possible.
  • Do not translate variable names, paths or technical identifiers.

Variable & style conventions

  • Use UPPER_CASE for script-level variables.
  • Use lower_case for local function variables (declare with local).
  • Quote all variable expansions: "$VAR" — not $VAR.
  • Use [[ ]] for conditionals, not [ ] (except where POSIX is required).
  • show_proxmenux_logo is the appropriate way to clear the screen — it includes the clear and shows the project logo so the user always has visual context. Call it once at the start of Phase 2 (and optionally before a long Phase 1 spinner block).

Standard variable names across the project:

CTID         # container ID
VMID         # virtual machine ID
DISK         # device path (e.g. /dev/sdb)
PARTITION    # partition path (e.g. /dev/sdb1)
STORAGE      # Proxmox storage name
MOUNT_POINT  # filesystem mount path

Redirecting tool output during Phase 2

Phase 2 displays a clean log of msg_info → msg_ok lines accumulating on screen. If a tool you call (apt, mkfs, qm, pct, dd, etc.) writes its own output to stdout/stderr, it scrolls past your messages and breaks the visual flow — you end up with a chaotic terminal where the user can't tell the script's own progress lines from the underlying tool's noise.

Without redirect — what the user sees if you don't handle the noise:

 Installing kernel package...
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  proxmox-kernel-6.5
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 12.5 MB of archives.
After this operation, 47.2 MB of additional disk space will be used.
Get:1 http://download.proxmox.com/debian/pve trixie/pve-no-subscription amd64 ...
... [200+ more lines of dpkg output] ...
 Kernel installed.

With redirect — what the same operation looks like when noise is sent elsewhere:

 Installing kernel package...
 Kernel installed.
 Configuring kernel command line...
 Configured.
 Refreshing boot loader...
 Boot loader updated.

Two patterns to choose from:

  • Discard the output when you don't need it — fastest, simplest:
    DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" >/dev/null 2>&1
  • Send the output to a log file when you may want to inspect it later (debugging a failed install, checking what dpkg actually did). This is the preferred pattern for any apt operation:
    apt-get install -y "$package" >> "$log_file" 2>&1

The script scripts/global/update-pve9_2.sh is a reference implementation — every apt-get call sends output to a log file so the user only sees the clean msg_info → msg_ok flow, while the log on disk lets you reconstruct exactly what apt did if anything goes wrong.

Do's and Don'ts

✅ Do

  • stop_spinner before a dialog in Phase 1 only when a msg_info spinner is currently running.
  • Phase 2 starts with show_proxmenux_logo + msg_title + msg_ok recap of Phase 1 results.
  • Use msg_ok for successfully enabled features — even if a reboot is required.
  • Use whiptail (not dialog) for any post-execution prompt that must appear in Phase 2.
  • Always include a “No” branch in reboot dialogs that warns the user not to start the affected resource until rebooted.
  • Guard VM-only logic by checking [[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] — controllers and NVMe PCIe can't be added to LXC containers.
  • Use ensure_repositories from utils-install-functions.sh instead of unconditional apt-get update.
  • Use parallel arrays in Phase 1 when each item needs multiple dialogs.

❌ Don't

  • Call dialog while a spinner is active.
  • Skip the Phase 1 recap at the start of Phase 2.
  • Call show_proxmenux_logo a second time — it erases everything Phase 2 has printed.
  • Use dialog in Phase 2 (use whiptail for the rare unavoidable case).
  • Use bare clear.
  • Wrap msg_title in echo "" blank lines — it already includes spacing.
  • Use msg_warn to report a successfully enabled feature — that's an msg_ok.
  • Run unconditional apt-get update — use ensure_repositories.

Submitting your contribution

  1. Fork MacRimi/ProxMenux on GitHub and clone your fork.
  2. Create a feature/* branch from develop (see Branching model above).
  3. Follow this guide for any new or modified scripts. Write the header with your name; add the optional sponsor line if you want.
  4. Test your script on a real Proxmox VE instance — both Phase 1 (every dialog branch) and Phase 2 (every operation succeeds and rolls back cleanly on error).
  5. Open a Pull Request against develop with a clear description: what the script does, what changed, which Proxmox VE version it was tested on.
  6. Make sure your contribution respects the Code of Conduct.

For security-sensitive issues, follow the disclosure flow in SECURITY.md rather than opening a public issue.

Where to next

  • CONTRIBUTING.md (full guide) — the source of truth for every convention, including the advanced hardware patterns.
  • Contributors — the people whose work has shaped ProxMenux releases. (Different page from this one.)
  • Code of Conduct — the standards every contributor agrees to follow.
  • GitHub Discussions — ask before you build if you're not sure whether an idea fits, or to find collaborators on a larger feature.