Skip to main content

Tmux Workspaces: Declare Your Working Environment Instead of Rebuilding It

A tmux session is usually presented as something that persists.

You start a long-running command, detach from the session, close the terminal, and return later. The process continues running, and the terminals remain arranged as you left them.

That is one of tmux's most important capabilities, particularly when working over SSH.

But persistence is not the only reason to preserve a terminal arrangement.

Different kinds of work tend to require different recurring setups. When developing an application, you might want the application running in one pane, its logs visible beneath it, a general-purpose shell beside them, and another window containing system or database tools.

When investigating a production problem, writing an article, reviewing an unfamiliar repository, or running a data pipeline, you may want an entirely different arrangement.

The individual workspace might take only a minute or two to construct.

The real inconvenience is that you have to reconstruct it repeatedly.

And once you have built it, the arrangement exists only as mutable runtime state. You can resize the wrong pane, close the wrong window, repurpose a shell, or destroy the session entirely. Tmux can preserve the session while it remains alive, but the intended structure exists nowhere outside that particular instance.

The solution is to stop treating the running session as the definition of the workspace.

Instead, declare the workspace.

Persistence is not declaration

A persistent tmux session answers this question:

How do I keep what is currently running?

A declared tmux workspace answers a different question:

How do I reliably begin this kind of work?

Those are not the same problem.

A persistent session preserves one particular instance of your environment. It contains the windows, panes, commands, output, and accidental changes that happen to exist at that moment.

A workspace declaration preserves the structure you intended to create.

That distinction gives us three related but separate concepts.

Persistence

Persistence keeps a live session available.

You detach, reconnect later, and find the same processes and terminals still running.

Restoration

Restoration attempts to recreate a previous session after tmux or the machine stops.

It asks:

What was open before, and how can I bring it back?

Declaration

Declaration describes the environment you want tmux to construct.

It asks:

What should this kind of work look like whenever I begin it?

This article is about the third.

Different work deserves different workspaces

Consider a typical application-development environment.

You might want:

application-development
├── app
│ ├── application process
│ └── general-purpose shell
├── monitoring
│ ├── application logs
│ ├── process monitor
│ └── resource usage
└── database
└── PostgreSQL shell

For a data-processing system, the structure might be different:

data-pipeline
├── pipeline
│ ├── ingestion process
│ ├── aggregation process
│ └── analysis process
├── monitoring
│ ├── queue depth
│ ├── recent errors
│ └── system resources
└── investigation
├── Python shell
└── SQL shell

For writing technical documentation, you might want:

article-writing
├── editor
│ └── repository
├── preview
│ └── development server
└── references
├── general shell
└── search commands

The important point is not the particular arrangement.

The important point is that these arrangements recur.

You are not creating an arbitrary set of panes each time. You are reconstructing a familiar visual environment for a familiar category of work.

That recurring structure deserves to be represented explicitly.

A workspace declaration

A declared tmux workspace can describe:

  • The session name
  • The windows in the session
  • The name of each window
  • The panes within each window
  • The working directory of each pane
  • The relative pane layout
  • Programs that should start automatically
  • Commands that should be prepared but not executed
  • The initial window and pane that should receive focus

For example:

my-app
├── app
│ ├── ~/projects/my-app
│ │ └── go run ./cmd/server
│ └── ~/projects/my-app
│ └── general shell
├── logs
│ ├── ~/projects/my-app
│ │ └── tail -f ./logs/application.log
│ └── ~/projects/my-app
│ └── htop
└── database
└── ~/projects/my-app
└── psql

Once this structure is declared, the running session becomes disposable.

You can resize panes, open temporary shells, replace commands, add windows, close windows, or destroy the entire session.

None of those actions damage the declaration.

The next time you launch the workspace, tmux reconstructs its intended starting state.

Building a workspace directly with tmux

You do not need another tool to create a declared workspace.

Tmux already exposes commands for creating sessions, windows, panes, layouts, and initial commands. A shell script can combine those commands into a reusable workspace definition.

Here is a complete example:

#!/usr/bin/env bash

set -euo pipefail

SESSION="my-app"
PROJECT="$HOME/projects/my-app"

if tmux has-session -t "$SESSION" 2>/dev/null; then
exec tmux attach-session -t "$SESSION"
fi

# Create the session and its first window.
tmux new-session \
-d \
-s "$SESSION" \
-n "app" \
-c "$PROJECT"

# Start the application in the first pane.
tmux send-keys \
-t "$SESSION:app.0" \
'go run ./cmd/server' \
Enter

# Create a shell beneath the application.
tmux split-window \
-v \
-t "$SESSION:app.0" \
-c "$PROJECT"

# Prepare a health-check command without executing it.
tmux send-keys \
-l \
-t "$SESSION:app.1" \
'curl -v http://localhost:8080/health'

# Create the monitoring window.
tmux new-window \
-t "$SESSION" \
-n "monitoring" \
-c "$PROJECT"

# Follow the application log.
tmux send-keys \
-t "$SESSION:monitoring.0" \
'tail -f ./logs/application.log' \
Enter

# Add a system monitor beneath the logs.
tmux split-window \
-v \
-t "$SESSION:monitoring.0" \
-c "$PROJECT"

tmux send-keys \
-t "$SESSION:monitoring.1" \
'htop' \
Enter

# Add a process view beside the system monitor.
tmux split-window \
-h \
-t "$SESSION:monitoring.1" \
-c "$PROJECT"

tmux send-keys \
-t "$SESSION:monitoring.2" \
'watch -n 2 "ps -eo pid,%cpu,%mem,cmd --sort=-%mem | head -20"' \
Enter

# Ask tmux to arrange the monitoring panes evenly.
tmux select-layout \
-t "$SESSION:monitoring" \
tiled

# Create a database window.
tmux new-window \
-t "$SESSION" \
-n "database" \
-c "$PROJECT"

# Prepare the database command without executing it.
tmux send-keys \
-l \
-t "$SESSION:database.0" \
'psql "$DATABASE_URL"'

# Return to the application window and focus the prepared command.
tmux select-window -t "$SESSION:app"
tmux select-pane -t "$SESSION:app.1"

exec tmux attach-session -t "$SESSION"

Save the script somewhere on your PATH, make it executable, and run it whenever you want that workspace.

chmod +x ~/bin/my-app-workspace
my-app-workspace

The first time it runs, it constructs the session.

If the session already exists, it attaches to the existing instance instead.

What the tmux commands are doing

One benefit of starting with a Bash script is that you learn the tmux primitives beneath any higher-level workspace tool.

new-session

tmux new-session -d -s "$SESSION" -n "app" -c "$PROJECT"

This creates a detached session named my-app.

It also creates the first window, names it app, and starts it in the project directory.

The -d flag matters because the rest of the script needs to continue constructing the workspace before attaching to it.

new-window

tmux new-window -t "$SESSION" -n "monitoring" -c "$PROJECT"

This creates another window inside the session.

Naming windows is valuable because it allows the workspace definition to target meaningful names such as app, monitoring, and database rather than relying entirely on numeric indexes.

split-window

tmux split-window -v -t "$SESSION:app.0" -c "$PROJECT"

This splits an existing pane.

The -v flag creates a vertical division, placing the new pane beneath the target pane.

The corresponding horizontal split uses -h:

tmux split-window -h -t "$SESSION:monitoring.1" -c "$PROJECT"

send-keys

tmux send-keys \
-t "$SESSION:app.0" \
'go run ./cmd/server' \
Enter

This sends text and then sends the Enter key, causing the command to run.

The same command without Enter has a different effect:

tmux send-keys \
-l \
-t "$SESSION:app.1" \
'curl -v http://localhost:8080/health'

The command appears at the shell prompt, but it is not executed.

The -l flag tells tmux to treat the value as literal text.

select-layout

tmux select-layout -t "$SESSION:monitoring" tiled

This applies one of tmux's predefined pane arrangements to the selected window.

That can be easier than manually calculating pane sizes.

select-window and select-pane

tmux select-window -t "$SESSION:app"
tmux select-pane -t "$SESSION:app.1"

These commands determine where you begin when the workspace opens.

Initial focus is part of the workspace's structure. If one pane contains a prepared command or a checklist, it may be the most useful place to put the cursor.

Executed commands and prepared commands

The ability to prepare a command without executing it is especially useful.

Some actions are safe and routine enough to run automatically:

tmux send-keys \
-t "$SESSION:monitoring.0" \
'tail -f ./logs/application.log' \
Enter

Other actions benefit from a deliberate pause:

tmux send-keys \
-l \
-t "$SESSION:database.0" \
'psql "$DATABASE_URL"'

The second command is placed at the prompt, but you must press Enter yourself.

That gives you a chance to inspect the command, verify its arguments, modify it, or decide not to run it.

A workspace can therefore express several levels of readiness:

automatic
Start this immediately.

prepared
Type this, but wait for confirmation.

informational
Display instructions about what should happen next.

empty
Open a shell in the correct location.

That is more useful than treating every pane as either completely empty or fully automated.

A workspace can guide you without acting for you

A pane does not have to begin with a process.

It can begin with instructions.

For example:

tmux send-keys \
-t "$SESSION:database.0" \
"printf '\nBefore connecting:\n 1. Confirm the target environment\n 2. Check the expected configuration file\n 3. Verify the database hostname\n\n'" \
Enter

After printing the checklist, the script can prepare the next command:

tmux send-keys \
-l \
-t "$SESSION:database.0" \
'psql "$DATABASE_URL"'

The pane would then look roughly like this:

Before connecting:
1. Confirm the target environment
2. Check the expected configuration file
3. Verify the database hostname

$ psql "$DATABASE_URL"

Nothing important has happened yet.

The workspace has simply placed you at a useful decision point.

This is an important distinction. A workspace declaration does not need to silently configure every aspect of the environment. It can describe the visual structure, prepare likely actions, and leave operational decisions explicit.

The running instance is not precious

Without a declaration, a carefully arranged tmux session can become something you feel obligated to protect.

You may avoid closing a pane because reconstructing it would be annoying. You may leave a session running for weeks because it contains the arrangement you like. Over time, the session accumulates temporary shells, stale output, abandoned commands, and one-off experiments.

The workspace gradually becomes both valuable and messy.

A declaration changes that relationship.

The live session is no longer the authoritative copy of the workspace. It is merely one instance created from the definition.

That makes experimentation safer.

You can:

  • Resize panes for the current task
  • Replace a monitoring command
  • Open temporary windows
  • Close panes you no longer need
  • Destroy the session when the task is finished

The next launch reconstructs the original structure.

The declaration preserves the design. The session remains free to evolve.

Keep workspace definitions with the project

A project-specific workspace definition should usually live near the project it describes.

For example:

my-app/
├── cmd/
├── internal/
├── migrations/
├── scripts/
│ └── tmux-workspace.sh
├── web/
├── go.mod
└── README.md

That has several advantages.

The workspace can be version-controlled with the application. Changes to the application's processes, commands, or directory structure can be reflected in the same repository. Someone cloning the project can inspect the workspace definition rather than relying on undocumented terminal habits.

For personal workspaces that span multiple repositories, a dedicated configuration directory may make more sense:

~/.config/tmux/workspaces/
├── application-development.sh
├── database-investigation.sh
├── infrastructure-debugging.sh
├── article-writing.sh
└── interview-practice.sh

These two approaches are not mutually exclusive.

A repository can contain its canonical development workspace, while your personal configuration contains broader workspaces tailored to your own machine and preferences.

Make the script idempotent

A workspace launcher should account for the possibility that the session already exists.

The simplest behavior is to attach to it:

if tmux has-session -t "$SESSION" 2>/dev/null; then
exec tmux attach-session -t "$SESSION"
fi

Another option is to fail and require the caller to decide what happens:

if tmux has-session -t "$SESSION" 2>/dev/null; then
printf 'Session already exists: %s\n' "$SESSION" >&2
exit 1
fi

A third option is to destroy and rebuild the session:

tmux kill-session -t "$SESSION" 2>/dev/null || true

That last behavior should be explicit. Automatically destroying an active session can terminate useful processes.

The important thing is that the launcher's behavior should be predictable. Running the same command twice should not accidentally create duplicate windows, split the wrong pane, or attach to an unrelated session.

When Bash starts becoming configuration

A shell script is an excellent place to begin.

It exposes the underlying tmux model directly. You learn how sessions, windows, panes, targets, layouts, and key injection work. There is no additional abstraction to understand.

For a small number of workspaces, that may be all you need.

As the definitions grow, however, several problems can appear.

Pane targeting becomes harder to read:

tmux send-keys -t "$SESSION:monitoring.2" ...

Shell quoting becomes increasingly unpleasant:

tmux send-keys \
-t "$SESSION:monitoring.2" \
'watch -n 2 "ps -eo pid,%cpu,%mem,cmd --sort=-%mem | head -20"' \
Enter

Repeated setup logic accumulates across scripts. Layout structure becomes distributed across a sequence of commands rather than visible as one hierarchy. Error handling and idempotency add more shell code.

Eventually, the script begins acting like a configuration file written in Bash.

That is the point at which a purpose-built workspace tool may become useful.

Purpose-built tmux workspace tools

Tools such as Tmuxinator and tmuxp allow tmux workspaces to be expressed using a more declarative configuration format.

A conceptual YAML definition might look like this:

session_name: my-app
start_directory: ~/projects/my-app

windows:
- window_name: app
layout: main-horizontal
panes:
- go run ./cmd/server
- null

- window_name: monitoring
layout: tiled
panes:
- tail -f ./logs/application.log
- htop
- watch -n 2 "ps -eo pid,%cpu,%mem,cmd --sort=-%mem | head -20"

- window_name: database
panes:
- null

The structure is easier to see at a glance.

Windows are represented as windows. Panes are represented as panes. Commands appear beneath the panes in which they run.

The purpose-built tool translates that declaration into tmux commands.

This offers several advantages:

  • Less repeated orchestration code
  • More readable pane hierarchies
  • Easier sharing across projects
  • Standard conventions for launching named workspaces
  • Less manual handling of tmux targets and layout construction

The tradeoff is that the abstraction may not expose every tmux behavior as directly as a shell script.

A Bash script can perform arbitrary checks, print instructions, conditionally choose commands, inspect files, or integrate with other local tools. A structured configuration format may be cleaner but less flexible.

Neither approach is universally better.

The useful progression is:

Learn the tmux commands

Build a workspace with a shell script

Notice which parts are repeated

Adopt a higher-level tool when it provides real value

Starting with native tmux gives you a better understanding of what any workspace tool is actually doing.

Templates are not resurrection

A declared workspace is different from a saved-session restoration tool.

A restoration tool attempts to recover the session you previously had:

session state at time A

save

restore

similar session state at time B

A workspace template creates a new instance from an intentional definition:

workspace definition

instantiate

fresh running workspace

The difference is subtle but important.

Restoration preserves what happened to exist.

Declaration preserves what was intended.

The previous session may contain useful state, but it may also contain accidental state: temporary panes, obsolete processes, old output, abandoned commands, and layouts that were altered for a one-time task.

A template gives you a stable starting point instead.

Both approaches can be useful. They simply solve different problems.

What about environment state?

There is a separate question beneath all of this:

When tmux starts a new session or pane, which environment variables does the new process inherit?

That question matters because a visually fresh workspace is not necessarily a clean process environment. Tmux has its own server-level and session-level environment behavior, which means a newly created pane may inherit state that is not obvious from the workspace layout.

That deserves a separate article.

For the purpose of this article, the workspace declaration is concerned primarily with visible structure:

  • Which windows exist
  • How their panes are divided
  • Where each pane begins
  • Which commands are displayed
  • Which commands run
  • Where your attention begins

The visual workspace and the process environment are related, but they should not be conflated.

Beyond tmux: a workspace should be a recipe

Desktop operating systems often provide something called a workspace or virtual desktop.

These are useful for grouping currently open windows. You might place a browser on one desktop, an editor on another, and communication tools on a third.

But that model usually treats a workspace as a location:

These are the windows currently placed here.

A declared tmux workspace behaves more like a recipe:

When I request this environment, construct it in this form.

That is a deeper and more useful abstraction.

A location holds whatever happens to be present.

A recipe describes how to produce a known arrangement.

The distinction applies beyond terminals. A complete declarative work environment could theoretically include:

  • Terminal windows and pane layouts
  • Editor workspaces
  • Browser profiles and tabs
  • Development tools
  • Database clients
  • Monitoring dashboards
  • Window positions
  • Initial instructions
  • Project-specific shortcuts

GUI applications make this more difficult because the meaningful state inside each application is often application-specific. A window manager may know where a browser window is positioned, but not necessarily which tab should be inspected, which development tools should be open, or which filter should be selected.

Tmux is unusually well suited to this model because its relevant components are explicit and addressable:

session
└── window
└── pane
├── working directory
└── command

That makes the workspace comparatively easy to describe, version, reconstruct, and destroy.

The desired structure is persistent

The most useful tmux workspace is not necessarily the one that lives forever.

It is the one you can recreate whenever you need it.

That shifts persistence away from runtime state and toward the workspace definition itself.

Persistent workspace definition

Fresh workspace instance

Temporary modifications

Disposable running session

The live session can change because the structure that produced it remains available.

You no longer need to preserve a particular arrangement by keeping it alive indefinitely. You preserve the recipe instead.

Persistence protects the workspace you have.

Declaration preserves the workspace you meant to have.

And once the intended structure is declared, the running session is free to be temporary.

Comments

No comments yet. Be the first!