1. Introduction

Without users, Linux isn’t very useful. Further, the $HOME directory is a key part of the user information. As such, knowing when and how it’s set can be important for the correct function of many scripts and commands.

In this tutorial, we explore the $HOME environment variable, how it works, and how its value changes. First, we talk about environment variables, as well as their behavior and source in general. Next, we concentrate on the $HOME variable. After that, we go through the user login process with regards to $HOME. Finally, we demonstrate how the shell home variable influences a common shell feature.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. It should work in most POSIX-compliant environments unless otherwise specified.

2. Environment Variables

Linux environment variables dictate the context of all processes:

$ env
SHELL=/bin/bash
PWD=/home/baeldung
LOGNAME=baeldung
XDG_SESSION_TYPE=tty
MOTD_SHOWN=pam
HOME=/home/baeldung
LANG=en_US.UTF-8
LS_COLORS=[...]
SSH_CONNECTION=192.168.6.66 60666 192.168.6.67 66
XDG_SESSION_CLASS=user
TERM=xterm-256color
USER=baeldung
SHLVL=1
XDG_SESSION_ID=1666
XDG_RUNTIME_DIR=/run/user/0
SSH_CLIENT=192.168.6.66 60666 66
XDG_DATA_DIRS=/usr/local/share:/usr/share
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/0/bus
SSH_TTY=/dev/pts/1
_=/usr/bin/env

In this case, we use the env command to show the variables set for the current shell session.

Consequently, the behavior of a given command or application can change depending on the current environment. For example, the LANG and other locale variables ensure the correct encoding and interpretation of characters.

Although they are often system-wide, each environment variable and its value may come from a different process.

3. $HOME Environment Variable

The $HOME environment variable contains the current user’s home directory. How that comes to be depends on several factors.

3.1. Initialization

When creating a user via useradd, the command doesn’t create their home directory automatically:

$ useradd user1
$ cd /home/user1
-bash: cd: /home/user1: No such file or directory

Yet, let’s check the entry in the /etc/passwd user database:

$ cat /etc/passwd | grep user1
user1:x:1001:1001::/home/user1:/bin/sh

Here, we can see that the sixth (6) colon-separated field for the user entry in /etc/passwd contains the home directory path. So, we already have this value stored in relation to our new user1.

3.2. Change Home

Naturally, we can change the home directory as well as the $HOME variable. However, understanding that these are two different configurations is critical.

Even if we change the contents of /etc/passwd manually or with a command, the $HOME variable value in all currently open sessions for the same user remain unchanged:

$ usermod --home /home/userone user1
$ cat /etc/passwd | grep user1
user1:x:1001:1001::/home/userone:/bin/sh
$ echo $HOME
/home/user1

Here, we employ usermod to change the –home path of the user1 entry in /etc/passwd to /home/userone. Still, the $HOME variable remains unchanged.

On the other hand, we can change the $HOME variable manually regardless of the original user home path in /etc/passwd:

$ HOME=/home/useruno
$ echo $HOME
/home/useredno
$ cat /etc/passwd | grep user1
user1:x:1001:1001::/home/userone:/bin/sh

This detachment will be crucial when understanding different ways to get home.

4. User Login and $HOME

Now, let’s log in as the user via su:

$ su user1
$ whoami
user1

As expected, the whoami command returns user1 as the current user after the switch.

At this point, we can check the environment for user1:

$ whoami
user1
$ env
MAIL=/var/mail/user1
USER=user1
SSH_CLIENT=192.168.6.66 60666 66
XDG_SESSION_TYPE=tty
SHLVL=1
MOTD_SHOWN=pam
HOME=/home/user1
OLDPWD=/
SSH_TTY=/dev/pts/1
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/0/bus
LOGNAME=user1
_=user1
XDG_SESSION_CLASS=user
TERM=xterm-256color
XDG_SESSION_ID=2666
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
XDG_RUNTIME_DIR=/run/user/0
LANG=en_US.UTF-8
LS_COLORS=[...]
SHELL=/bin/sh
PWD=/
SSH_CONNECTION=192.168.6.66 60666 192.168.6.67 66
XDG_DATA_DIRS=/usr/local/share:/usr/share

Notably, the value of $HOME* is already */home/user1.

Different login processes perform this depending on the session:

In summary, the relevant login program ensures it has read and supplied the home path as read from /etc/passwd to the shell or other initial process.

5. Rereading Home in the Shell

Notably, assigning a value to the $HOME variable that differs from the home path in the user entry from /etc/passwd may break some applications. Still, since it can happen after manual changes, let’s explore how the shell works in such cases.

5.1. Tilde Expansion

One feature that many shells offer is the tilde expansion:

$ echo ~
/home/user1

In short, the ~ tilde character has a special meaning and can expand to different paths:

+-----------------+--------------------------------------------------+
| Tilde Expansion | Value                                            |
+-----------------+--------------------------------------------------+
| ~               | current user home path                           |
| ~USERNAME       | home path of USERNAME                            |
| ~+              | current working directory                        |
| ~-              | previous working directory                       |
| ~#              | path number # from the path stack, left to right |
| ~-#             | path number # from the path stack, right to left |
+-----------------+--------------------------------------------------+

Of these, we’re mainly interested in the first two:

$ echo ~
/home/user1
$ echo ~user1
/home/user1

Let’s see what they match in terms of value:

$ echo $HOME
/home/user1
$ cat /etc/passwd | grep user1
user1:x:1001:1001::/home/user1:/bin/sh

In both instances, without manual modification, we get the home directory of user1 as set in $HOME and /etc/passwd.

5.2. ~* Values Sources

Now, let’s change the $HOME variable:

$ HOME=/home/useredno
$ echo ~
/home/useredno
$ echo ~user1
/home/user1

At this point, we see that ~ reflects the new value of $HOME, but ~user1 still shows the value from /etc/passwd. Let’s analyze this discrepancy.

5.3. Tracing

Since we expect the value of ~ to be read from a source that we open, let’s create a script and trace its system calls.

The executable tilde.sh script just specifies the interpreter and outputs the value of ~:

$ cat tilde.sh
#!/usr/bin/env bash
echo ~
$ chmod +x tilde.sh

Now, let’s use the strace command to see what calls the script makes in a regular environment where $HOME is set:

$ strace ./tilde.sh 2>&1 | grep '^open*'
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libtinfo.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/dev/tty", O_RDWR|O_NONBLOCK) = 3
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
openat(AT_FDCWD, "./tilde.sh", O_RDONLY) = 3

Here, we redirect stderr to stdout, which we then pipe to grep to get only lines that start with the open*() system calls. Notably, /etc/passwd isn’t mentioned, so only the internal $HOME is read.

On the other hand, if we unset the $HOME variable, the situation is different:

$ unset HOME
$ echo $HOME

$ ./tilde.sh
/home/user1
$ strace ./tilde.sh 2>&1 | grep '^open*'
[...]
openat(AT_FDCWD, "./tilde.sh", O_RDONLY) = 3
openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 3

This time, because $HOME is unset, the last two lines check the relevant database for the home directory and read it from /etc/passwd.

Notably, if we export the $HOME variable again, its value would be used by ~ tilde:

$ export HOME=value
$ echo ~
value

So, we can summarize our findings:

  • if $HOME is unset, ~ triggers a read from /etc/passwd
  • if $HOME is exported with any value, ~ uses that value

In particular, Bash uses the getpwent() function in the first case.

6. Summary

In this article, we talked about the user home directory and the source of its value.

In conclusion, the original user home directory is located in the /etc/passwd file but can also be cached in the $HOME variable of the shell.