Skip to content
Hoody.com

Every file is a URL. Read, download, hash, and browse files across local storage and 60+ cloud providers (Google Drive, Dropbox, S3, OneDrive, and others) through one HTTP interface.

Every Hoody container runs hoody-files, which provides that access wherever the files live.


hoody-files exposes storage as HTTP endpoints:

  • Web file manager - Visual file browser with a built-in code editor, and the main entry point for interactive file management
  • Read files - Stream content via HTTP from any mounted storage
  • Download files - With progress tracking and integrity verification
  • Get metadata - Size, type, modification time without downloading
  • Verify hashes - SHA256 integrity checking
  • List directories - Browse with sorting, filtering, multiple formats
  • Mount 60+ providers - Google Drive, S3, Dropbox, SFTP, WebDAV, and more
  • Response formats - HTML browser, JSON API, plain text for scripts
  • Archive preview - Inspect .tar.gz/.zip contents without extracting
  • Directory archives - Download any directory as .zip with the ?zip parameter
  • Base64 encoding - Embed files in JSON or data URLs

Each link below goes to the reference page for that endpoint, with all parameters, responses, and examples.

File reading and downloading:

  • GET /api/v1/files/{path} - Read/download file content
    • Query params: backend, base64
  • GET /{path} - Alternative endpoint with HTML/JSON/simple formats
    • Query params: json, simple, content-type
  • HEAD /{path} - Get metadata without downloading
    • Returns metadata headers (file size, type, modification time)

File integrity:

Archive operations:

  • GET /{path}?zip - Download directory as .zip archive
    • Recursively archives entire directory tree
    • Useful for quick backups or file transfers

Directory listing:

Backend management:

System monitoring:


Open the container files URL in a browser:

https://{project}-{container}-files-1.{server}.containers.hoody.com

The interface provides:

  • Visual folder navigation - Click folders to browse
  • Built-in search - Find files quickly
  • Sortable columns - Sort by name, size, date
  • Code editor - Edit text files directly in browser with syntax highlighting
  • Upload interface - Drag-and-drop file uploads (if permitted)
  • Download manager - Click to download files
  • Archive creation - Create .zip/.tar.gz directly
  • File preview - View images, PDFs, text files inline

Use it for daily file management, quick edits, browsing cloud storage, reviewing logs, and editing config files, none of which require leaving the browser. The interface is the same on a phone, a tablet, and a laptop.

The same request shape works against every mounted backend:

Terminal window
# Container files URL: https://{project}-{container}-files-1.{server}.containers.hoody.com
# Local container files
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/report.pdf"
# Google Drive
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/Work/report.pdf?backend=8f3a2c1e4b5d6f7a"
# Amazon S3
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/backups/data.zip?backend=2b9d4e6a1c3f5078"
# Dropbox
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/Photos/vacation.jpg?backend=7c1e9a3b5d2f4860"

Only the backend parameter differs between them.

Connect a storage provider once:

Terminal window
hoody files backends connect drive \
--client-id "your-app.apps.googleusercontent.com" \
--client-secret "your-secret" \
--token '{"access_token":"ya29..."}'

Response (201 Created). The echoed config masks values whose key matches a known-sensitive name (password, token, secret, client_secret, private_key and similar). It is a name list, not content inspection, so a credential stored under an unusual key comes back in the clear:

{
"success": true,
"message": "drive backend connected successfully",
"data": {
"id": "9b4d3e2f5a6c7890",
"type": "drive",
"vfs_backend_type": "drive",
"config": {
"client_id": "your-app.apps.googleusercontent.com",
"client_secret": "***",
"token": "***"
},
"mount_paths": []
}
}

The returned backend ID then addresses every Drive file:

Terminal window
# List Drive root
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/?backend=8f3a2c1e4b5d6f7a"
# Download Drive file
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=8f3a2c1e4b5d6f7a" \
--output report.pdf

The mount persists across container restarts, so you connect a provider once.

Pick the format that suits the caller:

Terminal window
# Interactive file browser
open "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/"

Interactive file browser with visual navigation, search, sortable columns, and upload interface (if permitted).

Check a download against the hash the service reports:

Terminal window
# Container files URL
FILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
# 1. Get expected hash
expected=$(curl -s "$FILES_URL/api/v1/files/large-file.bin?hash")
# 2. Download file
curl "$FILES_URL/api/v1/files/large-file.bin" -o large-file.bin
# 3. Verify
actual=$(sha256sum large-file.bin | awk '{print $1}')
if [ "$expected" = "$actual" ]; then
echo "✓ Download verified"
else
echo "✗ Download corrupted"
fi

This matters most for:

  • Production deployments
  • Backup verification
  • Large file transfers
  • Compliance requirements

Preview an archive without extracting it:

Terminal window
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/backup.tar.gz?preview"
# Lists contents without downloading full archive

Base64-encode a file for embedding:

Terminal window
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/config.json?base64"
# Returns: eyJrZXkiOiJ2YWx1ZSJ9

Override the content type:

Terminal window
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/data.txt?content-type=text/csv"
# Forces download as CSV

Download a whole directory as a zip:

Terminal window
# Download entire directory as .zip archive
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/?zip" \
-o documents.zip

The file service runs as root inside the container, but the files and folders it creates are owned by your container user (user), not root. The interactive shell and your processes can therefore read, edit, and delete everything the file manager makes.

This applies to every operation that creates a new inode: uploads, new folders (?mkdir), touch, appends to a new file, archive extraction (every extracted entry), copies, downloads (?download_from), and any parent directories created along the way. Existing files keep their owner: overwriting, appending to, or moving an existing file never changes who owns it.

Operator configuration (CLI flags / env on the file service):

FlagEnvDefaultMeaning
--default-create-owner <spec>HOODY_FILE_MANAGER_DEFAULT_CREATE_OWNERuserOwner for newly-created inodes. spec is user, user:group, uid, uid:gid, or none/off to disable (inherit the process owner = root).
--allowed-create-owners <csv>HOODY_FILE_MANAGER_ALLOWED_CREATE_OWNERS(empty)Owners a client owner= override may request (the default owner is always allowed). Resolved per request.

Ownership is fail-closed: when the feature is active the service verifies it can change ownership at startup, and if a per-operation ownership change unexpectedly fails the request returns 500 and the partially-created file/dir is rolled back: a created file is never silently left owned by root.


The journal records every mutation. Each time a file is created, written, appended, deleted, moved, copied, or has its permissions changed, the journal captures the event along with a content-addressable blob snapshot of the file at that moment. You can therefore read any file as it existed at any past revision, at any point in time, or compute diffs between any two versions.

The journal is best-effort: file operations always succeed even if journaling encounters an error. Under normal conditions every mutation is captured, which gives you a complete audit trail of your container’s filesystem.

See every revision of a file:

Terminal window
hoody files get src/app.ts --history --limit 50

Retrieve the exact content of a file at any point in its history:

Terminal window
# By revision number
hoody files get src/app.ts --revision 3
# By timestamp
hoody files get src/app.ts --at "2026-03-19T14:30:00Z"

Compute a unified diff between any two versions of a file:

Terminal window
hoody files get src/app.ts --diff --from-seq 1 --to-seq 3

Search across all mutations in your container and monitor journal health:

Terminal window
# Query recent writes under src/
hoody files query --path src/ --op write --limit 20
# View journal storage statistics
hoody files stats

Different tools for different storage:
- AWS CLI for S3
- Google Drive SDK
- Dropbox API
- SFTP client
- WebDAV client
Each with different authentication, different SDKs, different patterns.

That means:

  • Multiple tools to learn
  • Different APIs per provider
  • Complex authentication flows
  • No unified interface
  • Awkward access for AI, which needs a provider-specific SDK for each one
One HTTP API for everything:
https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/{path}?backend={provider}

That gives you:

  • One interface for 60+ providers
  • Consistent authentication (mount once)
  • Same HTTP patterns everywhere
  • Reachable by any AI that can make a standard HTTP request
  • Observable (all file access logged)
  • Embeddable (file browsers in iframes)
  • Script-friendly (simple text format)

A phone browser reaches the same files:

// From mobile browser
await fetch('https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/documents/contract.pdf?backend=8f3a2c1e4b5d6f7a')
.then(r => r.blob())
.then(blob => {
// View PDF on phone
});

The device needs no Google Drive app, no Dropbox app, and no S3 client, only the ability to make an HTTP request.


Read files from several providers in one pass:

// List all mounted backends
const filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
const backends = await fetch(filesUrl + '/api/v1/backends').then(r => r.json());
// Access files from each
for (const backend of backends.backends) {
const files = await fetch(filesUrl + `/api/v1/files/?backend=${backend.id}`)
.then(r => r.json());
console.log(`${backend.backend_type}: ${files.paths.length} files`);
}

Download a file and check it against the reported hash:

async function verifiedDownload(path, backend) {
// 1. Get expected hash
const hashResponse = await fetch(
`/api/v1/files${path}?backend=${backend}&hash`
);
const expectedHash = await hashResponse.text();
// 2. Download file
const fileResponse = await fetch(
`/api/v1/files${path}?backend=${backend}`
);
const blob = await fileResponse.blob();
// 3. Verify hash (browser)
const arrayBuffer = await blob.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
const actualHash = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
if (actualHash === expectedHash) {
console.log('✓ Download verified');
return blob;
} else {
throw new Error('Download corrupted - hash mismatch');
}
}

Confirm that every local file exists in the cloud backup:

import requests
files_url = 'https://{project}-{container}-files-1.{server}.containers.hoody.com'
def verify_backup(local_path, backup_backend):
# Get local listing
local = requests.get(f'{files_url}{local_path}?json').json()
# Get backup listing
backup = requests.get(
f'{files_url}/api/v1/files{local_path}',
params={'backend': backup_backend}
).json()
local_files = {p['name']: p['size'] for p in local['paths'] if p['path_type'] == 'File'}
backup_files = {p['name']: p['size'] for p in backup['paths'] if p['path_type'] == 'File'}
missing = set(local_files.keys()) - set(backup_files.keys())
size_mismatch = [
name for name in local_files
if name in backup_files and local_files[name] != backup_files[name]
]
if not missing and not size_mismatch:
print(f'✓ Backup complete: {len(local_files)} files verified')
else:
print(f'✗ Missing {len(missing)} files, {len(size_mismatch)} size mismatches')

An agent reads a file over HTTP and sends it to a model:

// AI agent reads code file
const filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
const code = await fetch(
filesUrl + '/api/v1/files/app/main.js?backend=8f3a2c1e4b5d6f7a'
).then(r => r.text());
// AI analyzes
const analysis = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{
role: 'user',
content: `Review this code:\n\n${code}`
}]
});
// AI can directly access your files from cloud storage
// No downloading to local machine needed

Compare the local and remote listings, then fetch what is missing:

#!/bin/bash
FILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
# Get local directory
local=$(curl -s "$FILES_URL/documents/?json")
# Get remote directory
remote=$(curl -s "$FILES_URL/api/v1/files/documents/?backend=2b9d4e6a1c3f5078")
# Compare and download missing files
echo "$remote" | jq -r '.paths[] | select(.path_type == "File") | .name' | \
while read filename; do
if ! echo "$local" | jq -e ".paths[] | select(.name == \"$filename\")" > /dev/null; then
echo "Downloading: $filename"
curl "$FILES_URL/api/v1/files/documents/$filename?backend=2b9d4e6a1c3f5078" \
-o "documents/$filename"
fi
done

One HTTP interface replaces the per-provider CLIs:

Terminal window
# Traditional: Different CLI for each provider
aws s3 cp s3://bucket/file.pdf ./ # AWS CLI
gcloud storage cp gs://bucket/file.pdf ./ # Google CLI
az storage blob download ... # Azure CLI
rclone copy dropbox:file.pdf ./ # Rclone
# Hoody: One HTTP interface
FILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
curl "$FILES_URL/api/v1/files/file.pdf?backend=2b9d4e6a1c3f5078" -o file.pdf
curl "$FILES_URL/api/v1/files/file.pdf?backend=5a7c9e1b3d2f4860" -o file.pdf
curl "$FILES_URL/api/v1/files/file.pdf?backend=6b8d0f2a4c1e3759" -o file.pdf
curl "$FILES_URL/api/v1/files/file.pdf?backend=7c1e9a3b5d2f4860" -o file.pdf

A phone browser reaches every mounted backend:

// Phone browser
const filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
const file = await fetch(
filesUrl + '/api/v1/files/documents/contract.pdf?backend=8f3a2c1e4b5d6f7a'
);
const blob = await file.blob();
// View PDF directly in mobile browser
// No app installation needed

Google Drive, S3, and Dropbox are all reachable from the mobile browser, because every file is an HTTP resource.

Embed a file browser in a page:

<!-- Live file browser in documentation -->
<iframe src="https://demo-files.hoody.com/examples/?backend=3d5f7a9c1e2b4068&readonly=true"
height="400" />

Readers see the actual files rather than a description of them.

Verify backups over HTTP:

#!/bin/bash
# Nightly backup verification
FILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
for file in $(curl -s "$FILES_URL/critical/?simple"); do
# Get hash from local
local_hash=$(curl -s "$FILES_URL/critical/$file?hash")
# Get hash from S3 backup
backup_hash=$(curl -s "$FILES_URL/api/v1/files/critical/$file?backend=2b9d4e6a1c3f5078&hash")
if [ "$local_hash" != "$backup_hash" ]; then
echo "WARNING: Backup mismatch: $file"
# Re-upload to S3 via other tools or trigger alert
fi
done

An agent lists, reads, and categorizes files:

const filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
// AI agent lists files
const files = await fetch(filesUrl + '/api/v1/files/?backend=8f3a2c1e4b5d6f7a')
.then(r => r.json());
// AI analyzes and categorizes
for (const file of files.paths) {
if (file.path_type === 'File') {
const content = await fetch(
filesUrl + `/api/v1/files/${file.name}?backend=8f3a2c1e4b5d6f7a`
).then(r => r.text());
// AI determines category
const category = await ai.categorize(content);
// AI can move files, create folders, organize automatically
}
}

Move a file between cloud providers:

// Read from Google Drive
const file = await fetch(
'/api/v1/files/document.pdf?backend=8f3a2c1e4b5d6f7a'
).then(r => r.blob());
// Upload to S3 (via other tools/APIs)
// Or use hoody-files to bridge providers

Connect every provider when the container is created:

const providers = [
{ type: 'drive', credentials: googleCreds },
{ type: 's3', credentials: awsCreds },
{ type: 'dropbox', credentials: dropboxCreds }
];
for (const provider of providers) {
await fetch(`/api/v1/backends/${provider.type}`, {
method: 'POST',
body: JSON.stringify(provider.credentials)
});
}
// Now all storage accessible through one interface

For production data, system backups, or compliance checks:

Terminal window
hash=$(curl -s "$URL?hash")
curl "$URL" -o file
echo "$hash file" | sha256sum -c

Each format suits a different consumer:

  • HTML - Interactive browsing in browser
  • JSON - API integration, processing
  • Simple - Shell scripts, piping to other commands

Directory listings change infrequently:

const cache = new Map();
async function getDirectory(path, backend, ttl = 60000) {
const key = `${path}:${backend}`;
const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttl) {
return cached.data;
}
const data = await fetch(`/api/v1/files${path}?backend=${backend}`)
.then(r => r.json());
cache.set(key, { data, time: Date.now() });
return data;
}

Check that a backend is healthy first:

Terminal window
# Test connection
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/backends/{backend_id}/test"
# Check if successful before bulk operations

Cloud providers throttle API calls:

// Add delays between requests
for (const file of files) {
await downloadFile(file);
await new Promise(r => setTimeout(r, 100)); // 100ms delay
}
// Or use Cache backend to reduce provider API calls

How many cloud providers can I mount simultaneously?

Section titled “How many cloud providers can I mount simultaneously?”

Unlimited. Mount Google Drive, S3, Dropbox, OneDrive, Box, and 50+ others all in one container. Each gets a unique backend ID. Access files from any provider through the same HTTP interface.

Do mounted backends persist across container restarts?

Section titled “Do mounted backends persist across container restarts?”

Yes. Backend configurations are stored in the container’s filesystem. After a restart, all previously mounted storage is reconnected automatically, so you do not re-authenticate each time.

Can I mount the same provider multiple times?

Section titled “Can I mount the same provider multiple times?”

Yes. Different Google Drive accounts, S3 buckets, or Dropbox folders each mount as a separate backend with its own ID, which is how you keep multiple tenants or client accounts apart.

How fast are local files versus cloud files?

Section titled “How fast are local files versus cloud files?”

Local files: sub-millisecond access. Cloud files: 50-500ms depending on provider and distance. Use Cache backend to speed up frequently accessed remote files. Or sync important files locally.

Yes. An agent makes standard HTTP requests to hoody-files endpoints, so listing directories, reading files, and verifying hashes all work over plain HTTP with no provider-specific SDK.

Yes, but limited to local container storage or specific backends that support writing. Most cloud providers require OAuth scopes for write access. Check backend documentation for write capabilities.

Use container proxy permissions to control who can access files. Configure IP whitelist, password auth, or JWT validation. Until you configure permissions, access is gated only by knowledge of the URL — anyone holding it can reach the files.

Yes. Open the hoody-files URL in your mobile browser: the HTML format gives you a visual file browser, and the JSON format works for a custom mobile app. Google Drive, S3, and local container files are all reachable from the phone through the same interface.

What happens if backend credentials expire?

Section titled “What happens if backend credentials expire?”

File operations return 401 Unauthorized. Rotate the credentials using PUT /api/v1/backends/{id} with the fresh token or secret, which preserves the existing backend ID. If you need to change identity fields (host, user, type), delete the backend and reconnect with POST /api/v1/backends/{type} to get a new ID.


Problem: Cannot mount storage provider

Solutions:

  1. Verify credentials are correct:

    Terminal window
    # Check OAuth tokens haven't expired
    # Verify API keys are valid
    # Ensure client_id/client_secret match
  2. Check network connectivity:

Terminal window
hoody files backends test {backend_id}
  1. Review provider documentation:
    • Google Drive requires OAuth with drive.readonly scope
    • S3 needs correct region and credentials
    • Dropbox tokens have app-specific permissions

Problem: File exists but getting 404 response

Check:

  1. Path is case-sensitive:

    /Documents/file.pdf
    # Correct: /documents/file.pdf
  2. Verify file exists:

    Terminal window
    # List parent directory
    curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/?json"
  3. Backend ID is correct:

    Terminal window
    # List all backends
    GET /api/v1/backends
    # Use correct backend_id

Problem: Downloaded file hash doesn’t match expected

Possible causes:

  1. Incomplete download - Re-download with resume support
  2. File modified during download - Re-download to get latest
  3. Network corruption - Use TCP retransmission, verify network
  4. Wrong hash algorithm - Ensure using SHA256

Solution:

Terminal window
# Use curl resume support
curl -C - "$URL" -o file
# Verify again
echo "$expected_hash file" | sha256sum -c

Problem: Cloud provider returning too many requests error

Solutions:

  1. Add delays between requests:

    for (const file of files) {
    await fetch(fileUrl);
    await new Promise(r => setTimeout(r, 200)); // 200ms delay
    }
  2. Use Cache backend:

    Terminal window
    # Mount cache in front of provider
    POST /api/v1/backends/cache
    {
    "remote": "s3_backend:bucket",
    "chunk_size": "10M"
    }
  3. Batch operations when possible - Download multiple files in one session

Problem: Timeout or incomplete download for large files

Solutions:

  1. Use curl with resume support:

    Terminal window
    curl -C - "https://{project}-{container}-files-1.{server}.containers.hoody.com/large-file.bin" -o large-file.bin
  2. Check disk space before downloading:

    Terminal window
    size=$(curl -sI "$URL" | grep Content-Length | awk '{print $2}')
    available=$(df -P . | tail -1 | awk '{print $4}')
    # Ensure available > size before downloading
  3. Download in chunks if supported:

    Terminal window
    # Some backends support Range requests
    curl -r 0-104857600 "$URL" > part1 # First 100MB
    curl -r 104857601- "$URL" > part2 # Rest
    cat part1 part2 > complete-file

The other data services in the kit:

SQLite

Serverless databases over HTTP: queries, a KV store, and time-travel reads.

Explore SQLite →

Exec

Scripts become HTTP endpoints, so your code is served as an API.

Explore Exec →

cURL

Complex HTTP operations reduced to a single GET request against any REST API.

Explore cURL →

More on file operations: