Skip to content

Add TRAMP support for remote agent execution#205

Open
csheaff wants to merge 7 commits intoxenodium:mainfrom
csheaff:gh-122-add-tramp-support
Open

Add TRAMP support for remote agent execution#205
csheaff wants to merge 7 commits intoxenodium:mainfrom
csheaff:gh-122-add-tramp-support

Conversation

@csheaff
Copy link

@csheaff csheaff commented Jan 18, 2026

Fixes #122

I've tested this with Claude Code and Copilot CLI. Unfortunately I realized in the process that I need persistent sessions with remote, as I often times lose connection. So SSH isn't ideal. But perhaps this PR will be useful for others with stable connections.

- Add agent-shell--tramp-command-runner to run agents on remote hosts via SSH
- Add agent-shell--resolve-tramp-path to convert paths between TRAMP and remote-local format
- Add agent-shell-enable-tramp-support and agent-shell-disable-tramp-support commands
- Fix temporary-file-directory returning remote path when default-directory is TRAMP path (icon caching bug)

Closes xenodium#122
For remote TRAMP sessions, save transcripts to ~/.agent-shell/transcripts/<host>/<path>/
instead of the remote filesystem. This avoids slow TRAMP file operations on every append.
- Add agent-shell--local-temp-directory helper for cross-platform temp paths
- Fix fallback icon caching to use local temp directory
- Use ~/tmp instead of /tmp for Windows compatibility
- Add ERT tests for TRAMP functions
- Add README.org documentation for TRAMP support
- Error explicitly on multi-hop TRAMP paths (prevents silent wrong-host execution)
- Use locate-user-emacs-file for cache instead of ~/tmp
- Add declare-function for tramp-file-name-hop
- Add negative ERT tests for multi-hop and unsupported methods
- All 23 tests pass
@xenodium
Copy link
Owner

Thank you for kicking this off!

I need to start breaking the agent-shell.el sections into sub packages. Mind if we move the TRAMP functionality to a new file, say agent-shell-tramp.el?

ps. I'm not using TRAMP myself, so may be good to ping the feature request at #122 and ask for feedback or folks to try it out.

Move TRAMP-specific functions to a separate file per maintainer request:
- agent-shell--tramp-command-runner
- agent-shell--resolve-tramp-path
- agent-shell-enable-tramp-support
- agent-shell-disable-tramp-support
- agent-shell--tramp-transcript-dir

Keep agent-shell--local-temp-directory in main file as it's needed
for icon caching even without TRAMP support.

Add autoloads for enable/disable commands.
@csheaff csheaff mentioned this pull request Jan 20, 2026
@fritzgrabo
Copy link
Contributor

fritzgrabo commented Jan 20, 2026

This is awesome -- love how support for containers and now TRAMP make agent-shell such a flexible tool for use with agents.

@csheaff re. the unstable connection issues you mention in the issue, maybe have a look at detached.el, and more importantly, the dtach tool it uses under the hood. It certainly adds another layer of indirection, but would help the session survive a reconnect.

Re. the PR, I was wondering if we could make TRAMP transparent, in the sense that it prefixes the command to run and resolves paths to/from remote machines automatically? After all, you should be able to defer from the current cwd and file path whether it's using TRAMP or not, so could bake that into the default logic, without the need to use agent-shell-container-command-runner or agent-shell-path-resolver-function at all.

UPDATE: just realized that not everybody would want to run the agent on the remote machine. which is likely why you went with the custom functions to manually toggle the feature -- got it 💡. The PR's issue mentions how eglot runs the server on the remote machine transparently, and if I read the code right, that happens automatically for remote working directories (at least I didn't find anything in eglot.el that would prefix the command to run with ssh ...). Should the same behavior should be the default in agent-shell then, and opting out of it should be toggled by a flag?

What do you think?

@csheaff
Copy link
Author

csheaff commented Jan 20, 2026

Hey @fritzgrabo , thanks for the feedback. In addition to removing the disable/enable functionality, I've attempted to use make-process as Eglot does. However agent-shell stalls for some reason and I need to move on to other work for rest of the week. @xenodium This requires some updates to acp.el i think:

  • adding :file-handler t to the make-process call in acp.el (line 130)
  • addressing the fact that TRAMP's file handler isn't compatible with the stderr handling in acp.el ( eglot.el line ~1794: uses (get-buffer-create ...) for stderr, not a pipe process)

I will attempt to fix next weekend if i can find some time.

@junyi-hou
Copy link

I tried it a little bit and here's what I found:

  • the ssh HOST -- claude-code-acp trick does not work if claude-code-acp is install in a customized place because this does not source the profile/rc file on the remote, changing the prefix to ssh HOST -- zsh -lc (or bash lc) fix this. i.e.
(defun agent-shell--tramp-command-runner (buffer)
  "Return command prefix for running commands on TRAMP remote host.
BUFFER is the agent-shell buffer.
Returns nil for non-TRAMP buffers, allowing local execution."
  (require 'tramp)
  (with-current-buffer buffer
    (let ((cwd (agent-shell-cwd)))
      (when (tramp-tramp-file-p cwd)
        (let* ((vec (tramp-dissect-file-name cwd))
               (method (tramp-file-name-method vec))
               (user (tramp-file-name-user vec))
               (host (tramp-file-name-host vec))
               (port (tramp-file-name-port vec)))
          (unless (member method '("ssh" "scp" nil))
            (error "TRAMP method '%s' not supported; only SSH is supported" method))
          (when (tramp-file-name-hop vec)
            (error "Multi-hop TRAMP paths not supported"))
          (append
           (list "ssh")
           (when port (list "-p" port))
           (list (if user (format "%s@%s" user host) host))
           (list "--" "zsh" "-lc")))))))
  • the environment variable set in agent-shell-anthropic-claude-environment doesn't seem to get pass onto the remote process. I was trying to add (when agent-shell-anthropic-claude-environment agent-shell-anthropic-claude-environment) to the list but this doesn't work with complex env variable (like ANTHROPIC_CUSTOM_HEADERS=OpenAI-Organization: xxx)

@junyi-hou
Copy link

environment variables can be passed in the following way:

(defun agent-shell-tramp-pass-env-var (var)
  "VAR should be output from `agent-shell-make-environment-variables`"
  (mapcar (lambda (env)
            (if-let* ((env-list (string-split env "="))
                      (value (cadr env-list))
                      (_ (string-match-p " " value)))
                (string-join `(,(car env-list) ,(format "'%s'" value)) "=")
              env))
          var))

to add quotes to the env variables

@neonmei
Copy link

neonmei commented Jan 25, 2026

Hi! first of all, thank you a lot for contributing on this 🙌

I took the branch for a spin to see how it works with gemini and qwen. Just as general context I do have some odd paths because I use nixos, but that generally works out with some adjusting of tramp-remote-path.

The steps I performed:

  1. I installed the gh-122-add-tramp-support branch in Doom Emacs and ensured that agent-shell-enable-tramp-support ran.
  2. opened a golang project on one of my local microvm.nix VMs (projectile-switch-project -> /ssh:ouroboros.vm:~/kaifa/go/yinglong). These generally work fast and functionality like LSPs, dape, etc run pretty well.
  3. picked a file and ran agent-shell
  4. said a "hello!" in the chat

for gemini, it seems stuck in Initializing but don't understand why

Edit: just cleaned up some caches and gemini seems to start! but it does exhibit same behavior as qwen for linked files.

As for qwen, it managed to start the shell with C-c a a and it answered the "hello", but current opened file gets linked as docs/docs/control-plane.org instead of docs/control-plane.org. Perhaps there's some quirk in path resolution for current opened buffer inside a remote project? (projectile, project.el, etc).

If anything needs some additional test, I'm happy to help!

@fritzgrabo
Copy link
Contributor

Great call on the ENV vars, much agree that it's important to be able to set those on the remote host.

TRAMP seems to have solved a lot of the issues around remote process execution already (including passing ENV vars and finding executables, see tramp-remote-process-environment and tramp-remote-path), so I believe that the more TRAMP functionality we can reuse here, the better: it'll keep the agent-shell code base simple(r) and allows users to fine-tune to their requirements using TRAMP built-in functionality.

Now that acp.el handles TRAMP via :file-handler (acp.el PR xenodium#9),
agent-shell no longer needs the SSH command prefix approach.

Removed:
- agent-shell--tramp-command-runner (acp.el handles this now)
- agent-shell-enable/disable-tramp-support commands
- SSH command runner tests

TRAMP now works automatically when default-directory is a TRAMP path.
Only the path resolver remains to convert paths between TRAMP and
local format for the agent.

Also fixed trailing underscore in transcript directory names.
@csheaff
Copy link
Author

csheaff commented Jan 26, 2026

Thanks everyone, I've pushed an update that changes the approach significantly.

What changed:

  • Now using TRAMP's native :file-handler mechanism (like eglot does) instead of the SSH prefix approach
  • This required changes to acp.el - see PR Add TRAMP support via file-handler acp.el#9
  • Removed agent-shell-enable/disable-tramp-support commands - TRAMP now works automatically when default-directory is a TRAMP path

@junyi-hou - The :file-handler approach should address both your concerns:

  1. TRAMP's file-handler respects tramp-remote-path, so custom install locations should work
  2. Environment variables are passed via tramp-remote-process-environment

@fritzgrabo - Agreed on using TRAMP built-ins. The file-handler approach inherently uses tramp-remote-process-environment and tramp-remote-path.

@neonmei - I wasn't able to reproduce the docs/docs/... path duplication. Could you try the latest push and let me know if you still see it? If so, what does (agent-shell-cwd) return when you're in the buffer? And what's the full TRAMP path of the file you have open?

@csheaff
Copy link
Author

csheaff commented Jan 26, 2026

Edit: I'm getting the stall again at

▶ in progress Starting agent

Creating client...

Subscribing...

Initializing...

not sure what's going on here...

@csheaff
Copy link
Author

csheaff commented Jan 26, 2026

Okay I think I fixed the stall issue. Seems it was a race condition - the SSH connection wasn't fully established before the first message was sent. The fix is in xenodium/acp.el#9

Copy link
Contributor

@fritzgrabo fritzgrabo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me, thanks for your updates!

Left a couple thoughts and nits in time for the weekend (sorry 😬).

(if (and (fboundp 'tramp-tramp-file-p)
(tramp-tramp-file-p default-directory))
;; When in a TRAMP buffer, use Emacs-standard cache directory
(locate-user-emacs-file "agent-shell/cache/")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be just (locate-user-emacs-file "cache/")?

Further down in lines 2213 and 2240, you're file-name-concat-ing another "agent-shell", so would end up with ".../agent-shell/cache/agent-shell".

;; TRAMP support is in agent-shell-tramp.el
(declare-function agent-shell--tramp-transcript-dir "agent-shell-tramp")

(defun agent-shell--local-temp-directory ()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you might want to consider naming this agent-shell--tramp-temp-dir and moving it into agent-shell-tramp.el. It's quite similar in spirit to agent-shell--tramp-transcript-dir imho.

(defcustom agent-shell-path-resolver-function nil
(autoload 'agent-shell--resolve-tramp-path "agent-shell-tramp")

(defcustom agent-shell-path-resolver-function #'agent-shell--resolve-tramp-path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought: since agent-shell--resolve-tramp-path does nothing for non-tramp paths (so is save to call in all cases), you might want to hardcode calling it into agent-shell--resolve-path, probably even before funcall-ing agent-shell-path-resolver-function. That way, TRAMP support would be entirely transparent.

@CeleritasCelery
Copy link

I just tested this with the acp.el updates, and it worked for me with claude code. Hope this can get merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tramp support

6 participants