diff --git a/core/attachment_storage.py b/core/attachment_storage.py
index f365fc9..4a12fe4 100644
--- a/core/attachment_storage.py
+++ b/core/attachment_storage.py
@@ -1,12 +1,13 @@
 """
 Temporary attachment storage for Gmail attachments.
 
-Stores attachments in ./tmp directory and provides HTTP URLs for access.
+Stores attachments to local disk and returns file paths for direct access.
 Files are automatically cleaned up after expiration (default 1 hour).
 """
 
 import base64
 import logging
+import os
 import uuid
 from pathlib import Path
 from typing import Optional, Dict
@@ -17,8 +18,10 @@ logger = logging.getLogger(__name__)
 # Default expiration: 1 hour
 DEFAULT_EXPIRATION_SECONDS = 3600
 
-# Storage directory
-STORAGE_DIR = Path("./tmp/attachments")
+# Storage directory - configurable via WORKSPACE_ATTACHMENT_DIR env var
+# Uses absolute path to avoid creating tmp/ in arbitrary working directories (see #327)
+_default_dir = str(Path.home() / ".workspace-mcp" / "attachments")
+STORAGE_DIR = Path(os.getenv("WORKSPACE_ATTACHMENT_DIR", _default_dir))
 STORAGE_DIR.mkdir(parents=True, exist_ok=True)
 
 
@@ -36,7 +39,7 @@ class AttachmentStorage:
         mime_type: Optional[str] = None,
     ) -> str:
         """
-        Save an attachment and return a unique file ID.
+        Save an attachment to local disk and return the absolute file path.
 
         Args:
             base64_data: Base64-encoded attachment data
@@ -44,9 +47,9 @@ class AttachmentStorage:
             mime_type: MIME type (optional)
 
         Returns:
-            Unique file ID (UUID string)
+            Absolute file path where the attachment was saved
         """
-        # Generate unique file ID
+        # Generate unique file ID for metadata tracking
         file_id = str(uuid.uuid4())
 
         # Decode base64 data
@@ -73,13 +76,19 @@ class AttachmentStorage:
             }
             extension = mime_to_ext.get(mime_type, "")
 
+        # Use original filename if available, with UUID suffix for uniqueness
+        if filename:
+            stem = Path(filename).stem
+            ext = Path(filename).suffix
+            save_name = f"{stem}_{file_id[:8]}{ext}"
+        else:
+            save_name = f"{file_id}{extension}"
+
         # Save file
-        file_path = STORAGE_DIR / f"{file_id}{extension}"
+        file_path = STORAGE_DIR / save_name
         try:
             file_path.write_bytes(file_bytes)
-            logger.info(
-                f"Saved attachment {file_id} ({len(file_bytes)} bytes) to {file_path}"
-            )
+            logger.info(f"Saved attachment ({len(file_bytes)} bytes) to {file_path}")
         except Exception as e:
             logger.error(f"Failed to save attachment to {file_path}: {e}")
             raise
@@ -95,7 +104,7 @@ class AttachmentStorage:
             "expires_at": expires_at,
         }
 
-        return file_id
+        return str(file_path)
 
     def get_attachment_path(self, file_id: str) -> Optional[Path]:
         """
diff --git a/gmail/gmail_tools.py b/gmail/gmail_tools.py
index 134d101..a9e0b67 100644
--- a/gmail/gmail_tools.py
+++ b/gmail/gmail_tools.py
@@ -908,57 +908,68 @@ async def get_gmail_attachment_content(
         )
         return "\n".join(result_lines)
 
-    # Save attachment and generate URL
+    # Save attachment to local disk and return file path
     try:
-        from core.attachment_storage import get_attachment_storage, get_attachment_url
+        from core.attachment_storage import get_attachment_storage
 
         storage = get_attachment_storage()
 
-        # Try to get filename and mime type from message (optional - attachment IDs are ephemeral)
+        # Try to get filename and mime type from message
         filename = None
         mime_type = None
         try:
-            # Quick metadata fetch to try to get attachment info
-            # Note: This might fail if attachment IDs changed, but worth trying
-            message_metadata = await asyncio.to_thread(
+            # Use format="full" to get attachment parts with attachmentId
+            message_full = await asyncio.to_thread(
                 service.users()
                 .messages()
-                .get(userId="me", id=message_id, format="metadata")
+                .get(userId="me", id=message_id, format="full")
                 .execute
             )
-            payload = message_metadata.get("payload", {})
+            payload = message_full.get("payload", {})
             attachments = _extract_attachments(payload)
+
+            # First try exact attachmentId match
             for att in attachments:
                 if att.get("attachmentId") == attachment_id:
                     filename = att.get("filename")
                     mime_type = att.get("mimeType")
                     break
+
+            # Fallback: match by size (attachment IDs are ephemeral)
+            if not filename and attachments:
+                for att in attachments:
+                    att_size = att.get("size", 0)
+                    if att_size and abs(att_size - size_bytes) < 100:
+                        filename = att.get("filename")
+                        mime_type = att.get("mimeType")
+                        break
+
+            # Last resort: if only one attachment, use its name
+            if not filename and len(attachments) == 1:
+                filename = attachments[0].get("filename")
+                mime_type = attachments[0].get("mimeType")
         except Exception:
-            # If we can't get metadata, use defaults
             logger.debug(
                 f"Could not fetch attachment metadata for {attachment_id}, using defaults"
             )
 
-        # Save attachment
-        file_id = storage.save_attachment(
+        # Save attachment to local disk - returns absolute file path
+        saved_path = storage.save_attachment(
             base64_data=base64_data, filename=filename, mime_type=mime_type
         )
 
-        # Generate URL
-        attachment_url = get_attachment_url(file_id)
-
         result_lines = [
-            "Attachment downloaded successfully!",
+            "Attachment downloaded and saved to local disk!",
             f"Message ID: {message_id}",
+            f"Filename: {filename or 'unknown'}",
             f"Size: {size_kb:.1f} KB ({size_bytes} bytes)",
-            f"\n📎 Download URL: {attachment_url}",
-            "\nThe attachment has been saved and is available at the URL above.",
-            "The file will expire after 1 hour.",
+            f"\n📎 Saved to: {saved_path}",
+            "\nThe file has been saved to disk and can be accessed directly via the file path.",
             "\nNote: Attachment IDs are ephemeral. Always use IDs from the most recent message fetch.",
         ]
 
         logger.info(
-            f"[get_gmail_attachment_content] Successfully saved {size_kb:.1f} KB attachment as {file_id}"
+            f"[get_gmail_attachment_content] Successfully saved {size_kb:.1f} KB attachment to {saved_path}"
         )
         return "\n".join(result_lines)
 
