So here's what Daniel sent us. He's running Claude Code in a terminal, bounded to a working directory. The agent hits something it can't do — an interactive login, like wrangler login for Cloudflare. It tells him to run it himself. He opens a second tab in Konsole, runs the command, completes the browser auth, comes back to the agent. And he says — I've since learned there's no sandbox between those two tabs, so don't spend time debunking me. Start from the corrected picture.
Good.
Two shells running as the same user on the same machine are not isolated from each other. wrangler login writes a credential file to disk. The agent's next command opens that same file. The work loops back for free. But here's where it gets interesting. What genuinely doesn't cross is the environment. If he exports a variable in his tab, the agent never sees it — because a process receives a copy of its environment at exec time, and there's no mechanism to push a change into a process that's already running. So the real dividing line isn't sandbox versus no sandbox. It's disk versus shell memory. He asks three specific questions. What is actually happening at the level of processes and memory when he opens a second shell — what does that new process inherit from its parent, what does it get its own private copy of, and is there any mechanism on Linux by which two already-running shells can share the in-memory part rather than each holding a copy that drifts?
That is a beautiful set of questions. And the wrangler login example is the perfect entry point because it makes the distinction visible in a way that a textbook explanation never would. The credential file lands on disk — both shells see it. The exported variable doesn't — and the reason why is the entire story of how Linux creates a process.
So let's trace it from the moment he opens that second tab.
Konsole spawns a new shell process. Under the hood, that's a fork followed by an exec. Fork creates a child process that is a near-exact copy of the parent — the memory image, the environment, the file descriptors are all duplicated. Then exec replaces that child's entire process image with a new program — bash or zsh or whatever shell he's using.
So the fork gives you a clone, and then exec immediately throws the clone's brain out and loads a new one.
And the environment — all those PATH and HOME and USER variables — gets handed to the new program at exec time. The exec family of functions takes the environment as an argument. If you don't pass one explicitly, it inherits the caller's environment. But critically, it inherits a copy. Not a reference. Not a pointer. A copy.
And that's the whole game right there.
It is. The environ man page — section seven — spells this out. Each process receives a copy of its parent's environment at exec time. After that, the two environments are independent. Change one, the other doesn't see it.
So when Daniel opens that second tab, what exactly does that new shell inherit from whatever process chain spawned it?
Several things. The environment array — copied, not shared. File descriptors — the terminal itself, which is why both tabs can read from and write to the same Konsole window. Each gets its own file descriptor table entry, but they point to the same underlying terminal device. The working directory — stored in the kernel's process structure as a reference to the directory's v-node. The umask. Signal dispositions. All inherited, and all are copies.
Wait — the working directory is a reference to the v-node, not just a string?
Correct. It's not a string that can go stale. But each process has its own record of what it considers its current working directory. Change it in one shell with cd, the other shell doesn't move.
Right, that one's obvious to anyone who's ever used two terminals. But it's the same principle as the environment — it just happens to be more visible.
And the file descriptor table is the subtle one. When fork duplicates the table, the child gets its own entries, but those entries point to the same open file descriptions in the kernel. So if a file descriptor was open before the fork, both processes can read from it, and the file offset is shared. That's how shell pipelines work.
So what Daniel's second shell does not get is any kind of live connection back to the first shell's memory. The environment was copied at exec time, and from that moment on, they're separate.
Completely separate. And this is where copy-on-write matters. When fork duplicates the parent's memory pages, it doesn't physically copy them at first. It marks them as read-only and shared. Only when one process writes to a page does the kernel make a private copy. So before exec, the child's environment is physically the same memory as the parent's. But the moment exec runs, the whole memory image is replaced anyway. And even if we stayed in the forked child without execing — the instant either process modifies an environment variable, copy-on-write kicks in and they diverge.
There's something almost philosophical about that. They start as the same bytes in the same physical memory, and the act of changing anything is what makes them separate.
It's the process equivalent of a branching timeline. And here's the practical upshot. When Daniel runs export FOO=bar in his tab, that modifies the environment of his shell process. The agent — Claude Code — is a different process, potentially a whole tree of processes, and every one of them received its own copy of the environment at its own exec time. None of them see FOO.
Which is why wrangler login works but export doesn't. The login command writes a file — typically to something like `~/.wrangler/config/default.That file sits on disk. The filesystem is the one thing that is shared between two processes running as the same user.
And it's worth being precise about what "shared" means here. The filesystem is a kernel-managed data structure. When process A writes to a file and process B reads from it, they're both making system calls through the kernel's virtual filesystem layer. The kernel maintains the page cache — file data in memory — and both processes see a consistent view because the kernel arbitrates access. wrangler login writes the credential token to disk, the kernel puts that data in the page cache, and when Claude Code's next command reads that config file, the kernel serves it from the same cache. No coordination between the processes required.
That's the "for free" part Daniel mentioned. And it's not a hack — it's the design. The filesystem is the universal shared medium on a Linux system. The one channel where two processes don't need to know about each other to exchange data. They just need to agree on a path.
Exactly.
Which brings us to the question Daniel didn't quite ask but is lurking under all of this. Is there any way to do the environment equivalent? Can two already-running shells share environment state in memory?
The short answer is no, not through standard mechanisms. There is no system call to modify the environment of another process. The environment is fixed at exec time. You can read another process's environment — it's exposed through /proc/pid/environ — but that file is generated on demand by the kernel from the target process's memory. Reading it doesn't create any kind of live link.
And writing to it?
Doesn't exist. /proc/pid/environ is read-only. Even if you could write to it, the target process has no mechanism to notice the change. Its environ pointer points to its own memory. It's not polling /proc. The environment is designed to be set up once at process start and then remain stable. If you want runtime configuration changes, you use a config file, or a Unix domain socket, or shared memory — mechanisms designed for inter-process communication. The environment is not an IPC mechanism.
Although — couldn't you use shared memory to implement environment sharing if you really wanted to?
You could, but it would require explicit cooperation from both processes. memfd_create creates an anonymous file that lives in memory and can be shared between processes by passing the file descriptor. You could write a shell that stores its environment variables in such a shared memory region instead of in a private array. Then another shell, if it had access to the same memfd, could read and write the same environment.
But no actual shell does this.
No actual shell does this. And for good reason. The environment is accessed constantly — every command lookup checks PATH, every program might read some config variable. Putting that behind a shared memory protocol with locking and consistency guarantees would be a performance nightmare compared to just reading a local array. It's technically possible in the sense that you could build it, but nobody has, because the cost would be enormous and the benefit tiny.
And the tools that seem like they might do this actually don't. tmux and screen — they share a terminal, not an environment. Each pane runs an independent shell process with its own environment. When you split a tmux window, it forks a new shell, which gets a copy of the environment at that moment. After that, they drift.
The connection is the terminal device, not the shell state. Tmux acts as a terminal multiplexer — it creates pseudo-terminals and routes input and output between them and the display. But each shell inside those pseudo-terminals is a separate process with its own memory, its own environment, its own working directory. Tmux doesn't mediate the environment at all.
What about direnv? That feels like it's doing something magical with environments across directories.
direnv is clever, but it's not sharing environment state between running shells either. It hooks into the shell's prompt evaluation. Every time the prompt is about to be displayed, direnv checks whether the current directory has an .envrc file, evaluates it, and exports any variables into the current shell. So it's re-evaluating and re-exporting on every prompt, not pushing changes into other running processes. Each shell runs its own direnv hook independently. It's a polling mechanism, effectively.
Let's go back to Daniel's Claude Code workflow, because there's a deeper point here about how AI agents and users coordinate. The disk-as-shared-medium pattern isn't an accident — it's the only thing that works reliably.
And it's what Claude Code is designed around. The agent runs in a bounded working directory. That directory is the shared context. When it needs you to authenticate, it tells you to run a command. That command writes credentials somewhere under that directory — or in a well-known location like ~/.wrangler/config/. The agent's next tool call can read that file. The loop closes. If Claude Code tried to do this through the environment, it would break immediately. The user exports a token, the agent never sees it, and both sides are confused.
Which is probably why the designers didn't even attempt it. The filesystem is the universal integration point. Every programming language can read a file. Every CLI tool writes its config somewhere predictable. There's no protocol to negotiate, no daemon to run, no shared memory to map. It's just paths and bytes.
There's a security implication here too, and Daniel alluded to it. Two shells as the same user are not isolated. Any process running as that user can read any file that user owns. The real boundary is user accounts, not terminal tabs. If you want isolation between two shells, you need them to run as different users, or in different containers, or in different namespaces. Two bash processes owned by the same UID live in the same security domain. They share the filesystem, they can signal each other, they can ptrace each other if ptrace_scope allows it. The terminal tab is a UI convenience, not a security boundary.
Which means when Daniel runs wrangler login in his tab and Claude Code reads the credential file in its tab, that's not a vulnerability. It's the expected behavior. The alternative — two same-user processes being unable to share files — would break nearly everything. If you do want to isolate an AI agent, you don't do it with terminal tabs. You do it with a container, or a separate user account, or a virtual machine.
So the mental model Daniel arrived at is exactly right. The dividing line is disk versus shell memory. Disk is shared. Environment is private per process. And the reason environment is private isn't because someone decided to isolate shells from each other — it's a consequence of how process creation works. The fork-exec model makes it inevitable. Every process starts as a copy of its parent, and then exec gives it a fresh image with a copied environment. There's no step in that sequence where the kernel says "and also, maintain a live link back to the parent's environment in case it changes."
And it's not a bug. It's a feature. Process isolation is what makes a multi-process operating system stable. If one shell's PATH change could corrupt another shell's command lookup, you'd have chaos. The drift between environments is actually useful. It means you can have one shell with a Python virtual environment activated, another with a Node version manager, and a third with neither — and they don't interfere.
Though that drift is also what causes the classic "it works in my terminal" bug, where someone exports a variable, forgets about it, and then a script run from a different context fails because that variable isn't set. Which is exactly why the disk pattern is more robust for anything that needs to cross process boundaries. A file on disk is either there or it isn't. There's no ambiguity about which process's copy is the canonical one.
Let me ask you something. If you were designing a new shell from scratch today, knowing that AI agents and users are going to be coordinating through the terminal, would you do anything differently with the environment model?
I think I'd keep the per-process environment exactly as it is — the isolation is too valuable. But I might add a first-class mechanism for a process to expose a "shared state" file descriptor that child processes can opt into. Something like memfd_create but with shell-level tooling around it. A publish-subscribe model for shell state. But honestly, the filesystem already does that job pretty well. The wrangler config file is a publish-subscribe model — wrangler publishes the credential, Claude Code subscribes by reading the file. The only thing missing is a notification mechanism so the subscriber doesn't have to poll.
inotify on the config file.
Right. And that's how some tools already work. But for the simple case Daniel described — run the login, come back to the agent, continue — polling on next command is perfectly adequate. The agent isn't sitting there waiting for the credential to appear. It's blocked on the user completing the interactive step. By the time the user comes back, the file is there. The human is the notification mechanism.
The slowest but most reliable message bus.
I want to circle back to something Daniel asked that we haven't fully addressed. What does the second shell inherit from its parent, and what does it get a private copy of? Let me be really explicit.
Go for it.
At fork time, the child inherits: the entire memory image — marked copy-on-write — which includes the environment array, the heap, the stack, and the program code. It inherits the file descriptor table, with each entry pointing to the same kernel file description as the parent's. It inherits the current working directory, the root directory, the umask, the signal dispositions, the process group ID, the session ID, the controlling terminal, the resource limits, the nice value, and the scheduling policy. It gets its own process ID and its own parent process ID. The child does not inherit: file locks held by the parent, pending signals, timers, or asynchronous I/O operations.
Then exec replaces the memory image entirely. The environment is passed to the new program as an argument.
And the exec family has several variants. execve takes the environment as an explicit array. execvpe and execlpe let you specify a custom environment. If you use execvp or execlp, the environment is inherited from the calling process's environ variable — but again, it's copied into the new process's address space. The kernel doesn't maintain a reference back to the caller. So the chain is: fork duplicates everything with copy-on-write, exec throws away the duplicate's memory and loads a fresh image with a copied environment, and from that point on the two processes share nothing in memory except what they explicitly set up through IPC — and the filesystem, which they share implicitly by virtue of running as the same user on the same kernel.
There's one more edge case I want to poke at. What about /proc/pid/environ? You mentioned it's read-only, but could a process theoretically poll its own /proc/self/environ and notice if someone else modified it?
Nobody can modify it. The /proc filesystem generates that file by reading the target process's environ pointer and copying the bytes into the read buffer. There is no write handler for that file. The kernel literally does not provide a code path to modify another process's environment through /proc. It's not a permission issue — the operation doesn't exist. Root can do something much more invasive — use ptrace to attach to the process, pause it, inject code into its address space, and manipulate its memory directly. But that's not an API — that's surgery. And it would be wildly unsafe. The process wasn't expecting its environment to change, so it might have cached values or made decisions based on the old state. If you need to change a running process's configuration, you send it a signal and have it re-read a config file. That's the Unix way.
The Unix way is also the reason Daniel's workflow just works. wrangler login writes a file. Claude Code reads the file. Neither process knows about the other. The kernel mediates the filesystem. The loop closes. And that loop works across any boundary that shares a filesystem — same machine, same user, but also across containers if you mount the same volume, across NFS if you're on different machines. The file is the universal intermediary.
Which makes me think the environment is almost the wrong metaphor for what Daniel was initially worried about. He started by asking if there's a sandbox between tabs. The answer is no — but the real insight is that the environment isn't a communication channel at all. It's a local configuration store, private to each process, set up at birth and never updated. The filesystem is the communication channel. Always has been. The wrangler workflow isn't exploiting a loophole — it's using the system as designed.
Let's talk about what this means for people building AI agent workflows. If you're designing a tool like Claude Code, what patterns should you use for sharing state between the agent and the user?
Files, files, and more files. A .env file for environment-like configuration that both sides can read. Credential files in well-known locations. A shared working directory where the agent's outputs and the user's inputs can both land. And if you need real-time coordination, a Unix domain socket or a named pipe — but those are more complex to set up and manage.
The .env file is interesting because it looks like environment variables but behaves like a file. It's the bridge between the two worlds.
Tools like direnv or dotenv libraries exist precisely to turn file-based configuration into environment variables at process startup. They read the file, export the variables, and then the process has its private copy. If the file changes later, the process doesn't see it unless it re-reads the file. The pattern is: write state to disk, read state from disk. The environment is a cache of disk state, not an independent source of truth. It's fast, it's private, and it goes stale. The disk is the source of truth.
I want to give listeners something concrete to try. Open two terminals. In the first, run env and note the output. Then export FOO=hello. Run env again and you'll see FOO. Now go to the second terminal and run env. FOO is not there. Then in the first terminal, run echo $FOO > /tmp/foo_test. In the second terminal, cat /tmp/foo_test. There's hello. Disk worked, environment didn't.
If you really want to see the process model in action, run strace on a shell and watch the fork and exec calls when you launch a command. You'll see the environment being passed as an argument to execve. It's right there in the system call trace. The other experiment worth doing is with /proc. Find the PID of your first shell. Run cat /proc/that_pid/environ from the second shell. You'll see the first shell's environment — including FOO — but as a null-separated blob. You can read it. You cannot change it. That's the whole story in two system calls. Fork duplicates, exec replaces, and the environment is a snapshot taken at birth.
Before we wrap up, I want to come back to Daniel's third question — the one about whether two running shells can share in-memory environment state. We said no through standard mechanisms. But I wonder if there's a future where this changes.
I doubt it. The copy-at-exec model is so fundamental to how Unix-like systems work that changing it would break an enormous amount of software. Every program assumes its environment is stable. Every shell script relies on the fact that exporting a variable only affects that shell and its children. If you introduced shared, mutable environments, you'd need a whole new set of APIs, and existing programs wouldn't use them. The answer isn't just "no, there's no mechanism." It's "no, and there probably never will be, because the current design is the right one for the problems the environment was meant to solve." The environment solves "how does a parent pass configuration to a child at startup." It was never meant to solve "how do two peers share state at runtime." For that, we have files, sockets, pipes, and shared memory. Different tools for different jobs.
The wrangler login workflow uses exactly the right tool for its job. The credential goes on disk. The agent reads it from disk. The environment stays out of it entirely.
It's almost too simple to notice. But that's the mark of a design that fits its problem well.
Hilbert: If I'm understanding this right — the whole reason export doesn't work between tabs but wrangler login does is that one writes to RAM and the other writes to disk. But doesn't the disk write also go through RAM in the page cache? What's actually different at the memory level?
That's a sharp question. The page cache is RAM — but it's RAM managed by the kernel as a shared resource. When wrangler login writes to disk, the data lands in the page cache, and the kernel makes it visible to any process that reads that file. When a shell exports a variable, it's writing to its own process's private memory pages — pages that are mapped into that process's address space and nobody else's. Same physical RAM chips, completely different access rules.
The key difference is the kernel's role. The page cache is a kernel data structure — any process making a read system call on that file gets served from the same cache. The environment array is in user-space memory — the kernel doesn't mediate access to it except through the narrow /proc read-only interface. So yes, both are ultimately in RAM. But one is shared by design, and the other is private by design.
Thanks, Hilbert. Good question.
The open question I'm left with is whether AI agent workflows will eventually drive new kernel features for process coordination. We're in this world now where humans and agents are sharing terminals and filesystems in ways the original Unix designers never imagined. The disk-as-shared-medium pattern works, but it's polling-based. Maybe we'll see something like a "process group environment" that multiple processes can subscribe to — not replacing the per-process environment, but sitting alongside it for the cases where you want shared state.
I suspect the filesystem will remain the answer for most things, simply because it's universal. Every tool speaks files. But I could see something like a standardized "session file" that tools like Claude Code and their users both know to read and write — a formalization of the pattern Daniel stumbled into.
Either way, the insight stands. The next time you're wondering why your export didn't take, or why your agent can't see your variable, remember: the environment is a snapshot, not a channel. The disk is the channel. That's the whole thing.
Thanks to our producer Hilbert Flumingtop. This has been My Weird Prompts. If you want to send us a question that's been nagging at you — especially one that exposes something fundamental about how your tools actually work — email the show at show at my weird prompts dot com.
We'll be back soon with more of your prompts.