Post

Tmux Cleanup Session Script | Automatically Kill Unused Tmux Sessions

Do you normally end up with around 15, 20 or more tmux sessions that you manually have to clean up? In this article I show you how I automatically clean up the tmux sessions I haven't used using a bash script, this means that you can run the script in macOS and also in Linux.

Tmux Cleanup Session Script | Automatically Kill Unused Tmux Sessions

Contents

Table of contents

YouTube video

If you like my content, and want to support me

  • I create and edit my videos in an M1 mac mini, and it’s starting to stay behind in the editing side of things, tends to slow me down a bit, I’d like to upgrade the machine I use for all my videos to a mac mini with these specs:
    • Apple M4 Pro chip with 14‑core CPU, 20‑core GPU, 16-core Neural Engine
    • 24GB unified memory
    • 1TB SSD storage
    • 10 Gigabit Ethernet
  • If you want to help me reach my goal, you can donate here

Image

Discord server

Image

Follow me on social media

  • The following links will be in the YouTube video description:
    • Each one of the videos shown
    • A link to this blogpost

How do you manage your passwords?

Image

Automatically clean up tmux sessions?

  • I use 1 tmux session per project. What is a project? It is each one of my GitHub repos
  • What this means is that with a single keymap I can switch between my different projects, or example, hyper+t+j takes me to my dotfiles, if I type hyper+t+u it takes me to my notes
  • Why do I have a project per tmux session? Because I can quickly switch to any of them, immediately open Neovim, start working on that project, and push my changes to GitHub when I’m done
  • The problem is that after a few days or weeks, I end up with around 15, or 20 tmux sessions. So I have to manually close them
  • To address this, I created a script that automatically kills the tmux sessions that haven’t been accessed in a certain amount of time
    • To be honest, I didn’t “create” the script, I modified a script that I found here
    • This script was created by the dhulihan GitHub user

Output of the script

  • Below you can see an example of the script in action
1
2
3
4
5
6
7
8
9
❯❯❯❯ cat /tmp/tmuxKillSessions.out
2025-03-12 23:53:21 - Killed session: dotfiles_latest-j (Inactive for 199min)
2025-03-12 23:53:21 - Killed session: karabiner_rules (Inactive for 199min)
2025-03-12 23:53:21 - Killed session: linkarzu-h (Inactive for 199min)
2025-03-12 23:53:21 - Killed session: obsidian_main-u (Inactive for 199min)
2025-03-13 09:04:59 - Killed session: karabiner_rules (Inactive for 204min)
2025-03-13 11:31:34 - Killed session: scripts_public- (Inactive for 224min)
2025-03-13 14:05:16 - Killed session: dotfiles_latest-j (Inactive for 176min)
2025-03-13 14:05:16 - Killed session: obsidian_main-u (Inactive for 132min)
  • Notice above that it has killed the sessions that have not been accessed for the amount of time specified in parentheses, I.E. (Inactive for 132min)
  • Notice that this file is stored in the tmp directory, which means it will be automatically deleted after a reboot

Script used

  • The script can be found below, just keep in mind that this may change in the future if I decide to update it, so to find the latest version, you can go to my dotfiles
  • Script found here: linkarzu/tmuxKillSessions.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env bash

# I (linkarzu) did not come up with this script, It's a slightly modified
# version of https://gist.github.com/dhulihan/4c65e868851660fb0d8bfa2d059e7967
# by github user dhulihan

# If I do not add this, the script will not find tmux or any other apps in
# the /opt/homebrew/bin dir. So it will not run the tmux ls command
export PATH="/opt/homebrew/bin:$PATH"

# I make this slightly lower than the LaunchAgent interval
TOO_OLD_THRESHOLD_MIN=110
TMUX_LOG_PATH="/tmp/tmuxKillSessions.log"

NOW=$(($(date +%s)))

tmux ls -F '#{session_name} #{session_activity}' | while read -r LINE; do
  SESSION_NAME=$(echo $LINE | awk '{print $1}')
  LAST_ACTIVITY=$(echo $LINE | awk '{print $2}')
  LAST_ACTIVITY_MINS_ELAPSED=$(((NOW - LAST_ACTIVITY) / 60))
  # # print all sessions
  # echo "${SESSION_NAME} is ${LAST_ACTIVITY_MINS_ELAPSED}min"

  if [[ "$LAST_ACTIVITY_MINS_ELAPSED" -gt "$TOO_OLD_THRESHOLD_MIN" ]]; then
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    echo "$TIMESTAMP - Killed session: $SESSION_NAME (Inactive for ${LAST_ACTIVITY_MINS_ELAPSED}min)" | tee -a $TMUX_LOG_PATH
    tmux kill-session -t ${SESSION_NAME}
    # In case you want to test the script without killing sessions, comment the 2 lines above and uncomment below
    # echo "${SESSION_NAME} is ${LAST_ACTIVITY_MINS_ELAPSED}min inactive and would be killed."
  fi
done
  • Notice that the command that does the trick is this one
1
tmux ls -F '#{session_name} #{session_activity}'
1
2
3
4
5
6
❯❯❯❯ tmux ls -F '#{session_name} #{session_activity}'
SSH-storage3-k 1741920331
dotfiles_latest-j 1741923284
linkarzu-h 1741923338
linkarzu_github_io- 1741923336
obsidian_main-u 1741922438
  • It shows you the name of the session, and it’s last activity time, then we just compare if the last activity is higher than our threshold TOO_OLD_THRESHOLD_MIN (notice that mine is set to 110 minutes) we kill the session
  • You could manually execute this script, but that wouldn’t make any sense, so instead, we’ll configure macOS to execute the script every 2 hours
  • If you’re on Linux, all you need to do is to setup a cron job and you’re good to go
  • In macOS it’s a bit trickier but it can be achieved creating a launch agent.

Automatically execute the script on macOS

  • Remember, as I mentioned above, we’ll do this using a LaunchAgent
  • I don’t like creating it manually, so I just add the code below to my .zshrc file, and if the file does not exist, it will create it. If the plist file is not loaded, it will load it
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  # Automate tmux session cleanup every X hours using a LaunchAgent
  # This will create plist file to run the script every X hours
  # and log output/errors to /tmp/$PLIST_LABEL.out and /tmp/$PLIST_LABEL.err
  # NOTE: If you modify the INTERVAL_SEC below, make sure to also change it in
  # the ~/github/dotfiles-latest/tmux/tools/linkarzu/tmuxKillSessions.sh script
  #
  # 1 hour = 3600 s
  INTERVAL_SEC=7200
  PLIST_ID="tmuxKillSessions"
  PLIST_NAME="com.linkarzu.$PLIST_ID.plist"
  PLIST_LABEL="${PLIST_NAME%.plist}"
  PLIST_PATH="$HOME/Library/LaunchAgents/$PLIST_NAME"
  SCRIPT_PATH="$HOME/github/dotfiles-latest/tmux/tools/linkarzu/$PLIST_ID.sh"

  # Ensure the script file exists
  if [ ! -f "$SCRIPT_PATH" ]; then
    echo "Error: $SCRIPT_PATH does not exist."
  else
    # If the PLIST file does not exist, create it
    if [ ! -f "$PLIST_PATH" ]; then
      echo "Creating $PLIST_PATH..."
      cat <<EOF >"$PLIST_PATH"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>$PLIST_LABEL</string>
    <key>ProgramArguments</key>
    <array>
        <string>$SCRIPT_PATH</string>
    </array>
    <key>StartInterval</key>
    <integer>$INTERVAL_SEC</integer>
    <key>StandardOutPath</key>
    <string>/tmp/$PLIST_ID.out</string>
    <key>StandardErrorPath</key>
    <string>/tmp/$PLIST_ID.err</string>
</dict>
</plist>
EOF
    fi
  fi

  # Check if the plist is loaded, and load it if not
  if ! launchctl list | grep -q "$PLIST_LABEL"; then
    echo "Loading $PLIST_PATH..."
    launchctl load "$PLIST_PATH"
    echo "$PLIST_PATH loaded."
  fi
  • Do you need to add this to your .zshrc file?:
    • No, you can create the plist file manually, you can see the code that my ~/.zshrc file generated in ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
1
cat ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
linkarzu.@.[25/03/13]
~/Library/LaunchAgents
❯❯❯❯ cat com.linkarzu.tmuxKillSessions.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.linkarzu.tmuxKillSessions</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/linkarzu/github/dotfiles-latest/tmux/tools/linkarzu/tmuxKillSessions.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>7200</integer>
    <key>StandardOutPath</key>
    <string>/tmp/tmuxKillSessions.out</string>
    <key>StandardErrorPath</key>
    <string>/tmp/tmuxKillSessions.err</string>
</dict>
</plist>
  • Only if you are adding the plist file manually, load it with the command below:
1
launchctl load ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
  • Why do I add this to my .zshrc file?
  • If you pay attention to the file, there’s a lot of verification steps I run, and I initialize a lot of the tools that I use. It’s a mess, I have to separate it in multiple files to make it easier to understand, but I set it up so long ago and it just works as of now, so why bother
  • Again, remember that the latest version of the code shown above is always going to be in my dotfiles, for now, my zshrc file can be found here:
  • Another big reason why my zshrc file is a bit complex, is because it helps me setup a new mac computer relatively easily.
  • I have a script that I execute when I get a new mac and I go over that in detail in this video:

Command: launchctl print

  • If I run this command, it shows me a lot of details about this launchctl agent
1
launchctl print gui/$(id -u)/com.linkarzu.tmuxKillSessions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
linkarzu.@.[25/03/13]
~/Library/LaunchAgents

❯❯❯❯ launchctl print gui/$(id -u)/com.linkarzu.tmuxKillSessions
gui/501/com.linkarzu.tmuxKillSessions = {
        active count = 0
        path = /Users/linkarzu/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
        type = LaunchAgent
        state = not running
        domain = gui/501 [100012]
        asid = 100012
        minimum runtime = 10
        exit timeout = 5
        runs = 7
        last exit code = 0

        spawn type = daemon (3)
        jetsam priority = 40
        jetsam memory limit (active) = (unlimited)
        jetsam memory limit (inactive) = (unlimited)
        jetsamproperties category = daemon
        jetsam thread limit = 32
        cpumon = default
        run interval = 7200 seconds
        probabilistic guard malloc policy = {
                activation rate = 1/1000
                sample rate = 1/0
        }

        properties = inferred program | system service | managed LWCR | tle system
}
  • Notice I can see that it has been executed 7 times runs = 7
  • I can also see the interval run interval = 7200 seconds and many more details
  • This quickly helps you to see if the launch agent is working or not

Change interval in which the script is executed

If adding the code to your zshrc file

  • First delete the existing plist file
1
rm ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
  • Then, re-create the file (in my case I just source my zshrc file)
1
source ~/.zshrc
  • Then I unload the plist file
1
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
  • Source my zshrc file again
1
source ~/.zshrc

If creating the plist file manually

  • Just modify the file directly
  • Then you’ll probably have to unload it
1
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist
  • And then load it again
1
launchctl load ~/Library/LaunchAgents/com.linkarzu.tmuxKillSessions.plist

Start your 14 day FREE trial

Start your 14 day FREE trial

Image

This post is licensed under CC BY 4.0 by the author.