Environment, User Identity, CWD, and umask
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=VALPatterns
| 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 missingIn 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 CWDumask 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
- Run the same binary from two different CWDs; print
Getwdand a relative open error. - Start a child with a stripped
Envand show it cannot findgitwithout PATH. - Write a file with mode
0o666and inspect actual mode withstat.