When you let an AI coding agent (Codex, Claude Code, or any CLI agent) operate on a remote Linux server over SSH, you eventually hit a wall the moment it tries to run sudo:
sudo: a password is required
The agent is not “broken.” It is simply running in a non-interactive shell, with no TTY attached, so sudo has nowhere to read the password from. Even if you know the password, you cannot type it into a remote process the agent spawned.
This post documents two practical ways to fix this. They solve the same symptom from very different angles.
The Root Cause
Non-interactive shell + sudo asking for password = deadlock agent ──ssh──> remote bash (no tty) ──> sudo ──> "Password:" ↑ nobody can answer
There are three theoretical escape routes:
- Make the operation not need sudo (file-level permissions / ACL)
- Make sudo stop asking for a password (
NOPASSWDin sudoers) - Force a pseudo-TTY and pipe the password (works locally, fragile remotely)
Approach 3 is rarely worth the trouble on a remote machine. Below are the two that actually scale.
Approach A — NOPASSWD in sudoers
Tell sudo to skip the password prompt for a specific user. The agent still calls sudo, but it goes through without authentication friction.
When to use
- The agent legitimately needs root-level operations across the system
- The target paths are spread across multiple root-owned locations
- You are comfortable granting (a scoped or full) passwordless root
Setup
Always edit sudoers through visudo — it validates syntax before saving. Direct vim /etc/sudoers* is dangerous: one typo can lock the entire sudo subsystem.
# Use visudo with vim as the editorsudo EDITOR=vim visudo -f /etc/sudoers.d/your-agent
Full passwordless root (simplest, but broadest):
your-user ALL=(ALL) NOPASSWD: ALL
Scoped to specific commands (safer):
Cmnd_Alias TARGET_OPS = \ /bin/cp * /opt/your-app/target-dir/*, \ /bin/mv * /opt/your-app/target-dir/*, \ /bin/mkdir /opt/your-app/target-dir/*, \ /bin/rm /opt/your-app/target-dir/*your-user ALL=(ALL) NOPASSWD: TARGET_OPS
Fix permissions (visudo usually does this automatically):
sudo chmod 440 /etc/sudoers.d/your-agentsudo chown root:root /etc/sudoers.d/your-agent
Verification
sudo su your-usersudo -n cp /tmp/src /tmp/dst
| Output | Meaning |
|---|---|
| Silent success | NOPASSWD is active |
sudo: a password is required | Config not picked up — check filename, syntax, permissions |
cp: cannot stat '...' | sudo already passed; this is a cp error, not a permission issue |
Filename rules for /etc/sudoers.d/
| Rule | Detail |
|---|---|
| Allowed characters | letters, digits, -, _ |
| Forbidden | . and ~ — sudo silently ignores files containing these |
| File mode | 0440, owner root:root |
| Load order | alphabetical; later files override earlier ones |
Trade-offs
- ✅ Simple, well-understood, standard for CI / agents
- ✅ Works for any command, including ones that need real root (package installs, service restarts)
- ⚠️
NOPASSWD: ALLeffectively gives that account unrestricted root - ⚠️ Hard to express “any operation on this directory” — sudoers matches on commands, not on paths as a primary concept
- ⚠️ Shell redirection (
>) and pipes are handled by the shell, not sudo, sosudo echo x > filedoesn’t work the way you’d expect
Approach B — POSIX ACL (setfacl)
Step back and ask the more useful question: does the agent actually need sudo, or does it just need write access to one directory?
If the answer is the latter, the cleanest solution doesn’t touch sudo at all. You give the user filesystem-level permission to the directory and the agent runs every command without sudo.
When to use
- The agent only needs to write to a specific directory tree
- You want to avoid granting any form of
sudo - You want shell redirection,
sed -i,git,rsync,tar, etc. to all “just work”
Setup
TARGET=/opt/your-app/target-dir# Grant rwx on existing files and directoriessudo setfacl -R -m u:your-user:rwx "$TARGET"# Default ACL so newly created files inherit the permissionsudo setfacl -R -d -m u:your-user:rwx "$TARGET"
The two commands are both required. Without the default ACL, files created inside $TARGET later will revert to the original ownership and the agent will lose write access to them.
Verification
getfacl /opt/your-app/target-dir
You should see entries like:
user::rwxuser:your-user:rwxdefault:user:your-user:rwx
Now the agent can do whatever it wants inside that tree, with no sudo prefix:
cp src.md /opt/your-app/target-dir/sed -i 's/old/new/' /opt/your-app/target-dir/*.mdecho "data" > /opt/your-app/target-dir/output.logrm /opt/your-app/target-dir/stale.tmpmkdir /opt/your-app/target-dir/subdir
Trade-offs
- ✅ Native semantics — “user X owns write rights to directory Y” is exactly what ACL expresses
- ✅ No sudoers changes, no security audit on
/etc/sudoers.d/ - ✅ Directory ownership stays
root, respecting FHS conventions for/opt/ - ✅ Redirection, pipes, and arbitrary tools all work
- ⚠️ A package upgrade that replaces files inside the target may strip ACLs from the new files
- ⚠️ Doesn’t help if the agent legitimately needs root for non-file operations (systemd, networking, etc.)
Side-by-Side
| Dimension | NOPASSWD sudoers | POSIX ACL |
|---|---|---|
| Conceptual axis | Authorizes commands | Authorizes files/directories |
Agent needs sudo prefix | Yes | No |
| Scope of trust | Broad (or carefully enumerated) | Tight (one directory tree) |
| Supports shell redirection | No (shell handles it before sudo) | Yes |
| Risk if account compromised | Potentially full root | Write access to one directory |
| Best for | System-wide tasks, package management, service control | Writing into a specific app/data directory |
| Setup complexity | Low–medium | Low |
| Maintenance | Grows as commands grow | One-time |
A Quick Decision Tree
Does the agent need root for anything beyond writing one directory?│├─ Yes → NOPASSWD in /etc/sudoers.d/│ (prefer scoped command list over `ALL` if you can)│└─ No → POSIX ACL with setfacl (no sudo involved, no sudoers risk)
A Note on Local vs Remote Agents
You may have noticed that the same agent running locally can happily prompt you for a sudo password mid-task, while the same agent over SSH cannot. That is not a bug, it is a property of the channel:
- Locally, the agent shares stdin with your terminal. When
sudowritesPassword:, the agent passes it to your screen, you type, the keystrokes go back into the child process. The TTY chain is intact. - Remotely, the agent’s SSH invocation typically runs in capture-stdout mode without an interactive TTY. The password prompt appears on the remote side, but there is no live keyboard wired to it.
That is the whole reason both approaches above are framed around “make the password go away” rather than “answer the password prompt.”
Conclusion
Two clean options, with different philosophies:
NOPASSWDis the right answer when the agent genuinely needs root and you want the standard SRE-style solution.- ACL is the right answer when the agent only needs to write somewhere, and you want the smallest, most surgical permission change.
For most “let my AI agent write into this one project directory” cases, ACL is the better starting point. Reach for NOPASSWD when the work genuinely crosses the root boundary.