ワンクリックで
wikimedia-toolforge
Manage Toolforge accounts, web services, Kubernetes pods, cron jobs, and file deployment for Wikimedia tools
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Manage Toolforge accounts, web services, Kubernetes pods, cron jobs, and file deployment for Wikimedia tools
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Deploy Python web services on Wikimedia Toolforge — Flask (WSGI) and FastAPI (ASGI), gunicorn/uvicorn, Build Service and traditional Kubernetes backends, virtual environments, pip caching, PORT configuration, static files, logging, and common pitfalls
Understand and work with English Wikipedia's WikiProject system — finding relevant projects, interpreting assessment tables, using Popular pages and work lists, and navigating project directories
Understand and query Wikidata — the free, collaborative, multilingual knowledge graph that underpins Wikipedia's inter-language links, Commons structured data, and semantic facts across all Wikimedia projects. Covers SPARQL, the Wikibase REST/Action APIs, RDF data dumps, and semantic web concepts
Query Wikidata by meaning, concept, or natural-language description — not just by exact label match. Uses semantic embeddings to find items (QIDs) and properties (PIDs) via vector similarity, keyword search, and Reciprocal Rank Fusion. Covers fuzzy semantic search, concept matching, similarity lookups, cross-lingual queries, and "find like this" when you do not know the exact QID or label
Design multilingual Toolforge tools — message files and ICU plurals, language detection and fallback chains, RTL/bidi layout, Unicode normalization and pitfalls, cross-wiki domain mapping, batch Wikidata label fetching, and avoiding English Wikipedia assumptions
Deploy and manage Node.js web services on Wikimedia Toolforge Kubernetes — zero-dependency server patterns, webservice commands, PORT configuration, static file serving with caching headers, npm on NFS, environment variables, logging, and common pitfalls
| name | wikimedia-toolforge |
| description | Manage Toolforge accounts, web services, Kubernetes pods, cron jobs, and file deployment for Wikimedia tools |
| license | MIT |
| compatibility | opencode |
| depends_on | ["wikimedia-api-access"] |
| skill_discovery_hints | [{"keywords":["Toolforge","tool hosting","Kubernetes","web service","cron job","deploy"]},{"keywords":["toolforge tools create","become","webservice","toolforge jobs"]},{"keywords":["CDN","cdnjs","tools-static","privacy-preserving CDN","content delivery","third-party script","external CDN tracking"]}] |
| last_verified | "2026-06-10T00:00:00.000Z" |
Toolforge (formerly Wikimedia Tool Labs) is a cloud hosting platform for community-developed tools that interact with Wikimedia wikis and data. This skill covers account setup, service management, deployment, and debugging.
ssh-agent running locally with your private key loaded (ssh-add ~/.ssh/id_ed25519)TOOLFORGE_USER — Your Toolforge shell/LDAP usernamessh ${TOOLFORGE_USER:-your-username}@login.toolforge.org hostname
If successful, you will see a hostname like tools-sgebastion-XX. If the connection hangs or fails, check that your SSH key is added to the admin console.
Each tool is a sub-account with its own directory, database access, and web service.
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org toolforge tools create my-tool-name
Naming rules:
pageview-analyzer, category-watchdog)After creation, the tool's home directory is at /data/project/my-tool-name/.
⚠️ Toolforge Rule #2 — Open Source License Required: All code in the Tools project must be published under an OSI-approved open source license. Add a
LICENSEfile to your repository before deploying. The absence of a license means default copyright laws apply, which is counter to the principles of the Wikimedia movement. See the full rules.
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org toolforge tools maintainers add my-tool-name ${TOOLFORGE_USER:-your-username}
This adds you as a maintainer so you can deploy files and manage services.
# Deploy a single file
scp my-script.py ${TOOLFORGE_USER:-your-username}@login.toolforge.org:/data/project/my-tool-name/
# Deploy an entire directory
rsync -avz --exclude '.*' ./my-tool/ ${TOOLFORGE_USER:-your-username}@login.toolforge.org:/data/project/my-tool-name/
Always use rsync for repeated deployments — it only transfers changed files and preserves permissions.
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
cd /data/project/my-tool-name/
git clone https://github.com/your-org/my-tool.git .
Then update with git pull on subsequent deployments. This provides version history and rollback.
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org chmod +x /data/project/my-tool-name/my-script.py
take command)Files copied via scp or rsync arrive owned by your shell user, not the tool user.
Many tool commands require tool-user ownership:
# File landed as wrong user after scp/rsync
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
take /data/project/my-tool-name/my-script.py
The take command changes ownership to the current tool user. It requires
that the calling user is the current owner (prevents abuse).
Toolforge supports several web service backends. Choose based on your needs:
| Backend | Use Case | Start Command |
|---|---|---|
webservice --backend=kubernetes python3.11 | Python web apps (Flask, Django, FastAPI) | webservice --backend=kubernetes python3.11 start |
webservice --backend=kubernetes node | Node.js web apps (Express) | webservice --backend=kubernetes node start |
webservice --backend=kubernetes php8.2 | PHP web apps | webservice --backend=kubernetes php8.2 start |
webservice --backend=kubernetes static | Static file serving (HTML/JS) | webservice --backend=kubernetes static start |
webservice --backend=buildservice | Build Service container (any language) | webservice --backend=buildservice start |
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
cd /data/project/my-tool-name
webservice --backend=kubernetes python3.11 start
The become command switches to the tool's service account, which has the correct permissions and environment variables.
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
webservice --backend=kubernetes python3.11 status
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
webservice --backend=kubernetes python3.11 stop
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
webservice --backend=kubernetes python3.11 restart
The web service looks for a server.py (Python), app.js (Node), or index.php (PHP) in the tool's home directory by default. To use a custom entry point, set the WEB_CONCURRENCY or create a launch.sh script:
#!/bin/bash
# launch.sh — custom web service entry point
cd /data/project/my-tool-name
gunicorn -w 4 -b 0.0.0.0:8000 my_app:app
Mark it executable: chmod +x launch.sh, then start with:
webservice --backend=kubernetes python3.11 start --launch launch.sh
For one-off or batch processing tasks that don't need a persistent web service:
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
toolforge jobs run my-job-name --command "python3 /data/project/my-tool-name/my_script.py" --image python3.11 --wait
| Option | Description |
|---|---|
--command "..." | Command to run inside the container |
--image python3.11 | Container image (choose based on language/runtime) |
--wait | Wait for the job to finish before returning logs |
--mem 2Gi | Memory limit (default: 1Gi, max: 4Gi) |
--cpu 1 | CPU cores (default: 1, max: 2) |
--filelog | Stream logs to a file on NFS (for later inspection) |
--timestamps | Add timestamps to log output |
become my-tool-name
toolforge jobs list
toolforge jobs logs my-job-name
become my-tool-name
toolforge jobs delete my-job-name
Kubernetes job containers run as the tool user, not root. The NFS home at
/data/project/<tool>/ is writable, but system directories are not:
| Can write to | Cannot write to |
|---|---|
/data/project/<tool>/ (NFS home) | /usr/local/ (root owned) |
/tmp/ (world-writable) | /var/lib/dpkg/ (root owned) |
$HOME (on NFS) | /etc/ (root owned) |
$TOOL_DATA_DIR | /data/ (root-owned) |
Consequences:
apt-get install anythingnpm install -g without setting NPM_CONFIG_PREFIX to a writable dir/data/ — use /data/project/<tool>/ or $TOOL_DATA_DIR insteadkubectl cp may fail with permission errors on the kubeconfig file
(use kubectl exec -i ... -- sh -c 'cat > /path' < localfile instead)For scheduled, recurring tasks:
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
crontab -e
Add a line in standard cron format:
# Run daily at 2:00 AM UTC
0 2 * * * /usr/bin/python3 /data/project/my-tool-name/daily_report.py >> /data/project/my-tool-name/logs/cron.log 2>&1
Cron job guardrails:
toolforge jobs (SOP 4) triggered by cron instead of running directlybecome my-tool-name
crontab -l
Toolforge provides a set-webservice-env command for web services:
become my-tool-name
toolforge env set MY_VARIABLE my_value
become my-tool-name
toolforge env list
become my-tool-name
toolforge env unset MY_VARIABLE
Store sensitive values as environment variables via toolforge env set. These are stored securely and not shown in env list output. Do not hardcode secrets in source files.
If your Toolforge tool authenticates against the MediaWiki API to make edits
(e.g., via bot passwords or Pywikibot), be aware that the API's lgname
parameter requires underscores where usernames have spaces:
# ✗ Fails silently with "Unknown error"
lgname = "AL Wiki MIT@mybot"
# ✓ Works
lgname = "AL_Wiki_MIT@mybot"
See the pywikibot skill ("Login fails" troubleshooting section) and the wikimedia-api-access skill ("Login Username Quirk" section) for full details.
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
webservice --backend=kubernetes python3.11 logs
Add --tail to see only the last N lines:
webservice --backend=kubernetes python3.11 logs --tail=50
become my-tool-name
toolforge jobs logs my-job-name
become my-tool-name
du -sh /data/project/my-tool-name/
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
# Now run commands interactively from the tool's home directory
python3 -c "import requests; print(requests.get('https://en.wikipedia.org/api/rest_v1/page/summary/Python_(programming_language)').json())"
The Toolforge Build Service allows you to build custom container images from a public Git repository using Cloud Native Buildpacks. This is the modern, recommended way to deploy tools — it frees you from per-language base images and gives you control over the runtime.
Instead of deploying files to NFS and using a language-specific webservice backend, you:
Procfile at the root of your repo defining how to start your apptoolforge build start to build a container image from your repositoryThe Build Service supports: Python, Node.js, PHP, Ruby, Go, Java/JVM, .NET, and Rust. It can also install OS-level Apt packages and compile frontend assets with Node.js at build time.
become access to your tool accountThe Procfile is a plain text file named Procfile (no extension) at the root of your repo:
web: gunicorn --bind=0.0.0.0 --workers=4 --forwarded-allow-ips=* app:app
migrate: python -m app.django migrate
web: entry defines what runs when you start a web servicemigrate:) define what runs as jobsImportant: Do not name a process type after a real command (e.g., do not use celery:
as a process type — use run-celery: instead).
ssh ${TOOLFORGE_USER:-your-username}@login.toolforge.org
become my-tool-name
# Build from the default branch (HEAD)
toolforge build start https://gitlab.wikimedia.org/toolforge-repos/my-tool
# Build from a specific branch, tag, or commit
toolforge build start --ref v1.2.0 https://gitlab.wikimedia.org/toolforge-repos/my-tool
# Use the latest buildpacks (newer language versions)
toolforge build start -L https://gitlab.wikimedia.org/toolforge-repos/my-tool
# Pass build-time environment variables
toolforge build start --envvar NODE_ENV=production https://gitlab.wikimedia.org/toolforge-repos/my-tool
become my-tool-name
# Start the web service from the built image
toolforge webservice buildservice start --mount=none
# Alternatively, create a service.template so 'webservice start' works directly:
echo -e 'type: buildservice\nmount: none' > service.template
# Check status
toolforge webservice buildservice status
# View logs
toolforge webservice buildservice logs -f
# Restart after a new build
toolforge webservice buildservice restart
# Stop
toolforge webservice buildservice stop
# Get an interactive shell inside the running container
toolforge webservice buildservice shell
Note on NFS mounts: By default, --mount=none is recommended. If your tool needs
to read/write files in /data/project/, use --mount=all and reference the path via
the $TOOL_DATA_DIR environment variable instead of relying on $HOME.
become my-tool-name
# Run the 'migrate' process from your Procfile
toolforge jobs run --wait --image my-tool/my-tool:latest --command "migrate" some-job
# Run with arguments
toolforge jobs run --wait --image my-tool/my-tool:latest --command "migrate --production" migrate-job
# Pass composite commands via shell wrapper
toolforge jobs run --wait --image my-tool/my-tool:latest --command "sh -c 'env; nodejs --version'" debug-job
become my-tool-name
# View the most recent build log
toolforge build logs
# Check build quota
toolforge build quota
To deploy a new version:
become my-tool-name
# 1. Push new code to your Git repo
# 2. Trigger a new build
toolforge build start https://gitlab.wikimedia.org/toolforge-repos/my-tool
# 3. Restart the web service to use the new image
toolforge webservice buildservice restart
Create a project.toml at the root of your repository:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.deb-packages]
install = [
"imagemagick",
"php",
]
Packages from Ubuntu 24.04 (Noble) can be looked up at https://packages.ubuntu.com/noble/.
id <user> will not work$HOME does not point to /data/project/<tool>/ — use $TOOL_DATA_DIR instead--mount=all explicitly when neededtoolforge build quota and request increases via Phabricator if neededUse become for interactive sessions, sudo -niu for SSH one-liners.
Running become my-tool-name interactively (SSH in, then become, then commands)
works correctly. But become uses exec internally, which replaces the shell —
so command chaining over SSH fails:
# ❌ BROKEN — second command runs as YOUR user, not the tool
ssh user@login.toolforge.org "become my-tool-name; webservice restart"
# ✅ CORRECT — use sudo -niu for chained SSH commands
ssh user@login.toolforge.org "sudo -niu tools.my-tool-name webservice restart"
SSH key expiry — Toolforge SSH keys expire after a period. If you get permission denied, regenerate your key in the admin console and re-add it to ssh-agent.
NFS latency — /data/project/ is on NFS. File operations can be slow. Avoid frequent small writes. Use local /tmp/ for temporary files and move results to NFS only when needed.
Resource limits — Kubernetes pods have 1 CPU and 1Gi RAM by default. Use toolforge jobs with --mem and --cpu flags for larger tasks. Do not run resource-intensive tasks on bastion or login nodes.
Do not run long processes on login — The login shell is for administration only. Long-running processes should be jobs or web services. Processes running for more than 30 minutes on login may be killed without warning.
Database connections — For replica database access, see the wikimedia-database skill. For tool-owned databases (MariaDB), use become my-tool-name and run sql my-tool-name to access the tool's database. If you need the actual MySQL username and password (e.g., for an external client or connection string), find them in the tool's home directory after become:
become my-tool-name
cat replica.my.cnf
This prints [client] with user and password fields. The sql command is still the recommended way to connect interactively, but replica.my.cnf is useful when configuring ORM connection strings or database drivers in application code.
Static file caching — Static web services (--backend=kubernetes static) serve from /data/project/my-tool-name/. Files are cached; wait a few minutes after deployment or use a versioned URL pattern (style.v2.css).
Test locally first — Deploying broken code to Toolforge wastes time. Test scripts locally with representative data before deploying.
Clean up old jobs — Kubernetes job history accumulates. Delete completed jobs that are no longer needed using toolforge jobs delete.
Build Service: Git repo required — The Build Service requires a public Git repository. Private repos are not supported. The Procfile must be at the repository root and named exactly Procfile (no extension). After a build, you must restart the web service to pick up the new image — toolforge build start alone does not restart running services.
Build Service: NFS and $HOME — Build Service containers do not have NFS mounted by default. Use --mount=all to mount it. Inside the container, $HOME does not point to /data/project/<tool>/ — use the $TOOL_DATA_DIR environment variable instead for tool home directory paths.
Wait ~1 minute after tool creation before become works. Use sudo -u as an immediate workaround.
Refs vary between browser sessions — When using Playwright or browser automation for toolsadmin, always snapshot to read the current accessibility tree instead of hardcoding element refs.
| Mistake | Symptom | Fix |
|---|---|---|
become <tool>; cmd1; cmd2 | Subsequent commands run as original user | Use sudo -niu tools.<tool> cmd1; cmd2 instead |
become <tool>; webservice restart | TLS cert errors or commands run as wrong user | Use sudo -niu tools.<tool> webservice restart |
--kubeconfig=PATH | stat .kubeconfig: no such file | Use --kubeconfig PATH (space, not =) |
toolforge env set | No such command 'env' | Use toolforge envvars create |
Nested " inside " over SSH | unexpected EOF | Use heredoc or alternate quote layers |
scp to /data/project/ | Permission denied (lands as wrong user) | Pipe through sudo; use take |
kubectl exec -it in non-tty | the input device is not a TTY | Use -i without -t, or add -t to the outer SSH command |
kubectl cp to a running pod | Permission errors reading kubeconfig | Use kubectl exec -i ... -- sh -c 'cat > /path' < localfile instead |
Writing to /data/ from inside a pod | Permission denied | Use /data/project/<tool>/ or $TOOL_DATA_DIR instead |
printf '\\e' for escape sequences | \e becomes literal escape char (0x1B) | Use printf '%s' '\\e' with %s format to output literally |
become <tool> sh -c "\$VAR" | Variable empty on bastion | Escaped $ passes through to the tool shell; unescaped expands on bastion |
# 1. Create tool
ssh user@login.toolforge.org toolforge tools create my-web-tool
# 2. Deploy code
rsync -avz ./my-web-app/ user@login.toolforge.org:/data/project/my-web-tool/
# 3. Set environment variables
ssh user@login.toolforge.org "become my-web-tool; toolforge env set API_KEY your-api-key-here"
# 4. Start web service
ssh user@login.toolforge.org "become my-web-tool; webservice --backend=kubernetes python3.11 start"
# 5. Verify it's running
ssh user@login.toolforge.org "become my-web-tool; webservice --backend=kubernetes python3.11 status"
# 1. Deploy the script
rsync -avz collect_data.py user@login.toolforge.org:/data/project/my-tool/
# 2. Set cron to trigger a Kubernetes job
ssh user@login.toolforge.org "become my-tool; crontab -l | { cat; echo '0 3 * * * toolforge jobs run daily-collect --command \"python3 /data/project/my-tool/collect_data.py\" --image python3.11 --wait --filelog >> /data/project/my-tool/logs/cron_trigger.log 2>&1'; } | crontab -"
# 1. Create tool
ssh user@login.toolforge.org toolforge tools create my-build-tool
# 2. Push code to a public Git repository (e.g., GitLab)
# Ensure a Procfile exists at the root:
# web: gunicorn --bind=0.0.0.0 --workers=4 app:app
# 3. Build the container image from the Git repo
ssh user@login.toolforge.org "become my-build-tool; toolforge build start https://gitlab.wikimedia.org/toolforge-repos/my-build-tool"
# 4. Start as a build service web service
ssh user@login.toolforge.org "become my-build-tool; toolforge webservice buildservice start --mount=none"
# 5. Verify it's running
ssh user@login.toolforge.org "become my-build-tool; toolforge webservice buildservice status"
# 6. To update: push new code to Git, rebuild, restart
# ssh login.toolforge.org "become my-build-tool; toolforge build start <repo-url>"
# ssh login.toolforge.org "become my-build-tool; toolforge webservice buildservice restart"
This skill includes helper scripts, reference docs, and templates:
scripts/deploy.sh)Deploy files to a Toolforge tool via rsync with dry-run preview.
./scripts/deploy.sh ./my-web-app my-tool-name
Features dry-run confirmation, permission setting, and post-deploy steps.
Note: For the Build Service (SOP 8), you do not use rsync deployment. Instead,
push code to a public Git repository and use toolforge build start. See the
Build Service workflow example above.
scripts/status.sh)Check web service status, Kubernetes jobs, disk usage, and active processes.
./scripts/status.sh my-tool-name
scripts/manage-k8s.sh)Manage Kubernetes jobs: run, list, logs, delete, and status.
./scripts/manage-k8s.sh my-tool-name run my-job "python3 /data/project/my-tool/script.py"
./scripts/manage-k8s.sh my-tool-name list
./scripts/manage-k8s.sh my-tool-name logs my-job
scripts/manage-cron.sh)Manage cron jobs: list, add, remove by pattern, or clear all.
./scripts/manage-cron.sh my-tool-name list
./scripts/manage-cron.sh my-tool-name add '0 2 * * *' 'python3 /data/project/my-tool/daily.py >> /data/project/my-tool/logs/cron.log 2>&1'
./scripts/manage-cron.sh my-tool-name remove daily.py
references/toolforge-cli.md)Quick reference of all Toolforge CLI commands organized by category:
assets/deploy-config.sh)Environment variable template for deployment scripts:
cp assets/deploy-config.sh my-config.sh
# Edit my-config.sh with your Toolforge username and tool name
source my-config.sh
./scripts/deploy.sh ./my-app my-tool-name
assets/app-template.py)Ready-to-deploy Flask app with:
/api/status)/api/summary/<title>, /api/search?q=...)cp assets/app-template.py server.py
# Edit, test locally with `python3 server.py`, then deploy
For Build Service deployment: Add a Procfile at your repo root containing:
web: gunicorn --bind=0.0.0.0 --workers=4 --forwarded-allow-ips=* app:app
Then push to a public Git repo and run toolforge build start.
Toolforge tools must load JavaScript, CSS, and fonts from Wikimedia's internal cdnjs mirror — never from external CDNs that track users.
External CDNs (cdnjs.cloudflare.com, unpkg.com, Google Fonts) can log IPs and track users. Wikimedia's Toolforge Web Hosting Policy requires the internal mirror for privacy compliance.
Only one hostname works reliably:
https://tools-static.wmflabs.org/cdnjs/ajax/libs/<library>/<version>/<file>
Examples:
<script src="https://tools-static.wmflabs.org/cdnjs/ajax/libs/d3/7.9.0/d3.min.js"></script>
<script src="https://tools-static.wmflabs.org/cdnjs/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css">
Search the cdnjs API to find versions, then construct the mirror URL:
# Search for a library
curl -s "https://api.cdnjs.com/libraries?search=d3&fields=version,latest"
# → {"name":"d3","version":"7.9.0",...}
# Construct mirror URL by replacing cdnjs.cloudflare.com/ajax/libs
# with tools-static.wmflabs.org/cdnjs/ajax/libs
| Do | Don't |
|---|---|
Use tools-static.wmflabs.org/cdnjs/ | Use cdnjs.cloudflare.com, unpkg.com, jsdelivr.net |
Pin exact versions (7.9.0) | Use latest or version-agnostic URLs |
| Verify availability before deploying | Assume all cdnjs libraries are mirrored |
The full CDN mirror guide with troubleshooting is at references/cdn-mirror-guide.md.
| File | Purpose |
|---|---|
scripts/check-cdn.sh | Verify a library is on the mirror (by name or full URL) |
scripts/list-available.sh | Search available libraries |
assets/load-template.html | HTML page loading jQuery, Bootstrap, Font Awesome from CDN |
assets/load-template.js | Dynamic JS loader for programmatic use |
Running commands on Toolforge involves nested shells: local → SSH → bastion → become/sudo → command. Each layer adds quoting complexity.
Local machine ──▶ SSH ──▶ Bastion shell ──▶ sudo -niu tools.<tool> ──▶ Command
Every ", $, \\, `, and ' must survive all layers.
# No special characters — works directly
ssh user@login.toolforge.org "sudo -niu tools.mytool toolforge jobs list"
# Single-quoted arguments inside the remote string
ssh user@login.toolforge.org "sudo -niu tools.mytool toolforge jobs run myjob --command 'python3 script.py'"
take Command Alternative for File TransferYou can't scp directly to /data/project/<tool>/ as your shell user
(it's owned by tools.<toolname>). Two approaches:
A) Staging area + take:
scp file.txt user@login.toolforge.org:/home/user/
ssh user@login.toolforge.org \
"sudo -niu tools.mytool sh -c 'cp /home/user/file.txt /data/project/mytool/ && take /data/project/mytool/file.txt'"
B) Pipe through SSH:
cat localfile | ssh user@login.toolforge.org \
"sudo -niu tools.mytool sh -c 'cat > /data/project/mytool/localfile'"
# Pipe a local file into a remote command as the tool user
cat my-script.sh | ssh user@login.toolforge.org \
"sudo -niu tools.mytool sh -c 'cat > /data/project/mytool/my-script.sh'"
# Heredoc piped through SSH
cat << 'EOF' | ssh user@login.toolforge.org \
"sudo -niu tools.mytool sh -c 'cat > /data/project/mytool/config.json'"
{
"key": "value"
}
EOF
Key insight for heredocs: If you quote the delimiter (<< 'EOF'), the content is treated as a literal string — no variable expansion happens locally. This is usually what you want when writing to remote files.
More reliable than kubectl cp (avoids kubeconfig permission issues):
# Pipe local content into a file inside a pod
cat config.json | kubectl exec -i my-pod -n tool-mytool -- sh -c 'cat > /tmp/config.json'
# Heredoc works the same way
cat << 'EOF' | kubectl exec -i my-pod -n tool-mytool -- sh -c 'cat > /tmp/.env'
API_KEY=sk-...
EOF
# Outer single quotes let you use double quotes inside for the remote command
ssh user@login.toolforge.org \
'sudo -niu tools.mytool kubectl exec pod -n tool-mytool -- sh -c "echo hello"'
The idiom 'single'\''quote' embeds a single quote inside a single-quoted string:
# IDIOM: 'text1'\''text2' produces: text1'text2
# ^^^^^^ ^^ ^^^^^^
# quoted \' quoted
# (escaped single quote between two quoted parts)
Common mistake — space after \':
# WRONG: 'text1'\'' text2'
# The space after \' STARTS a new argument
# Same for 'text1\' 'text2'
# RIGHT: 'text1'\''text2' — no space, all one argument
Safer alternative: build strings in multiple steps:
printf '%s' 'PS1=' >> /tmp/file # PS1=
printf '%s' '\\[\\e...' >> /tmp/file # the actual PS1 value
printf '%s' "'" >> /tmp/file # closing single quote
echo "" >> /tmp/file # newline
When you write a script that runs on the bastion itself (not from your local machine), there's one fewer shell layer, which is much simpler:
# /data/project/mytool/myscript.sh (runs as tools.mytool)
# Simple: direct kubectl with --kubeconfig (use SPACE, not =)
kubectl --kubeconfig /data/project/mytool/.kube/config get pods -n tool-mytool
# Multi-line script inside pod (escape $ for variables you want expanded IN the pod)
kubectl exec my-pod -n tool-mytool -- sh -c "
export PATH=/tmp/npm/bin:\$PATH
node --version
"
Critical rule for bastion scripts:
${VAR} (no escaping)\${VAR} or \$VAR (escaped $)| Related Skill | Why |
|---|---|
| wikimedia-database | SQL replicas — SSH tunnel setup shared with Toolforge |
| toolforge-nodejs | Node.js web services on Toolforge Kubernetes |
| toolforge-python | Python web services on Toolforge Kubernetes (Flask + gunicorn) |
| wikimedia-eventstreams | Real-time streams for monitoring tool events |
| wikimedia-i18n-l10n-for-tools | Multilingual design for Toolforge tools |
| wikimedia-ml-services | ML inference (Lift Wing) — often deployed as Toolforge services |
| wikimedia-phabricator | Report Toolforge bugs and track feature requests |
| wikimedia-security-and-privacy | Secrets management and data minimization for Toolforge tools |
| pywikibot | Bot framework — often deployed on Toolforge |