Environment, User Identity, CWD, and umask

Updated

September 8, 2026

Environment, User Identity, CWD, and umask

Overview

Every process inherits an environment block, a user/group identity, a current working directory, and (on Unix) a umask. Systems tools that ignore these surprise operators: relative paths resolve wrong, files get world-readable, and “works on my laptop” fails under systemd.

Environment variables

os.Getenv("HOME")
os.Setenv("APP_MODE", "prod") // process-local
os.LookupEnv("DEBUG")         // value, ok
os.Environ()                  // []string KEY=VAL

Patterns

Pattern Use
Config via env 12-factor services (DATABASE_URL)
Feature flags DEBUG=1, APP_LOG_LEVEL=debug
Child isolation cmd.Env = append(os.Environ(), "LC_ALL=C")
Clear inheritance cmd.Env = []string{"PATH=/usr/bin"} (explicit only)
cmd := exec.Command("git", "status")
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")

Never log secrets pulled from env (TOKEN, PASSWORD, *_KEY).

PATH and LookPath

path, err := exec.LookPath("docker")
// searches PATH; fails closed if missing

In minimal containers, PATH may lack tools your laptop has—document dependencies.

User and group identity (Unix)

fmt.Println("uid", os.Getuid(), "euid", os.Geteuid())
fmt.Println("gid", os.Getgid())
ID Meaning
UID / GID Real user/group
EUID / EGID Effective (permission checks)

Drop privileges in privileged starters (rare in pure Go services; more common in installers). Prefer running the whole process as non-root under Kubernetes/systemd.

u, err := user.Current()
// u.Username, u.HomeDir, u.Uid
// expand ~
if strings.HasPrefix(path, "~/") {
    home, _ := os.UserHomeDir()
    path = filepath.Join(home, path[2:])
}

Prefer os.UserHomeDir() / os.UserConfigDir() over hardcoding /home/....

Current working directory

wd, err := os.Getwd()
err = os.Chdir("/var/lib/myapp")

Relative paths (./data, config.yaml) are relative to CWD, not the binary location.

Binary-relative assets

exe, err := os.Executable()
dir := filepath.Dir(exe)
cfg := filepath.Join(dir, "config.yaml")

Note: os.Executable may be a symlink path; filepath.EvalSymlinks if you need the real directory.

Children and Dir

cmd := exec.Command("./tool")
cmd.Dir = "/work/repo" // child CWD

umask and file permissions

Unix umask masks permission bits on create. Go’s os.OpenFile / WriteFile pass a mode that is then masked by umask.

// request 0666 → often becomes 0644 with umask 0022
os.WriteFile("out.txt", data, 0o666)

For secrets:

f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)

Still subject to umask (e.g. 0o600 & ^umask). If you need exact bits, Chmod after create:

_ = os.Chmod(path, 0o600)

Hostname and process identity

host, _ := os.Hostname()
pid := os.Getpid()
ppid := os.Getppid()

Useful in logs: slog.Info("start", "host", host, "pid", pid).

Minimal example: show process context

func printContext(w io.Writer) {
    wd, _ := os.Getwd()
    fmt.Fprintf(w, "pid=%d ppid=%d\n", os.Getpid(), os.Getppid())
    fmt.Fprintf(w, "uid=%d euid=%d\n", os.Getuid(), os.Geteuid())
    fmt.Fprintf(w, "cwd=%s\n", wd)
    fmt.Fprintf(w, "home=%s\n", mustHome())
}

func mustHome() string {
    h, err := os.UserHomeDir()
    if err != nil {
        return ""
    }
    return h
}

Rules of thumb

Do Don’t
Resolve paths with UserConfigDir / absolute roots Assume CWD is the repo root in systemd
Pass explicit cmd.Env when isolation matters Leak host secrets into every child
Create secrets as 0600 + chmod Default world-readable state files
Log pid/host for multi-replica debug Log full os.Environ()

Try next

  1. Run the same binary from two different CWDs; print Getwd and a relative open error.
  2. Start a child with a stripped Env and show it cannot find git without PATH.
  3. Write a file with mode 0o666 and inspect actual mode with stat.