Use clit for mcp-less local tools in claude code.
Claude Code and MCP are a good pair if you need access to remote tools or commercial integrations, but it's overkill for local tools. Claude is already sitting on the command line, and that means all your favorite bash, python, and other scripts are available directly. Why wait for Claude to write JSON to call a server and have the server run the command?
Answer: One good reason to do this is to place firm limits on Claude's ability to execute arbitrary code. The authors of MCP tools thoughtfully include such safeguards. If you are working in a non-expendable environment, you should consider a more cautious approach.
... But looking around, I see a bunch of YOLO coders! Ain't nobody got time for that!
That's why I developed Command Line Interface Tooling, or CLIT for short.
Tl;dr
Put your tools scripts in a directory, put descriptions and instructions in a file, use Claude Code startup hook to cat or echo the file on startup, thus making Claude aware of tools with no client-server setup.
What is it?
At heart, all tools are merely instructions in the context of an LLM call. Everything is. When it comes to local tools, the only difference between frameworks like MCP and pasting in your instructions by hand is that the frameworks make discovery automatic. You fire up your agent and it “knows” what it can do without you having to tell it.
One form of this is CLAUDE.md, which Claude Code reads on startup. I've noticed though, Claude is so-so about being proactive with information in its CLAUDE.md. I don't know if this is due to the diverse kinds of information it can contain, or maybe training about how it is supposed to treat the file, but giving it a dedicated place to find tool information seems to give better results than adding the same information to CLAUDE.md.
Likewise, MCP gives the LLM the MCP config file with paths and commands for servers. Again, it's all text in context.
So, CLIT is simply another way of injecting instructions about how to run tools and where to find them. Instead of servers and MCP config files, you have your personal scripts and TOOLS.md.
How is it injected into context?
Claude Code has a hooks system for running commands at various points in the Claude workflow. It's a powerful system because it lets you run anything (read: DANGER, ARBITRARY COMMANDS). In our case we will be running... DRUMROLLLLLLLLL ... cat. Or echo, or print, or anything that puts text into stdout. All text in the context.
Yes. That's it. You make a file called TOOLS.md, create a startup hook for cat ~/TOOLS.md, and as soon as you launch Claude Code, the model is fed your instructions file. It can then execute the commands as needed throughout the remainder of your conversation.
Below I've provided screenshots of the full setup sequence, an example TOOLS.md, and a couple simple example tools: a GPT call tool and a Google CSE tool (CSE not included).
Setup
- Create TOOLS.md in your home directory or preferred location.
- Create a directory for your tools and copy the scripts into it, or run them from wherever they live with absolute paths. You know your system best.
- Add tool descriptions, instructions, and examples to TOOLS.md.
Example TOOLS.md
Here's a simplified example of the TOOLS.md file format:
# Direct Tool Execution System
## Available Tools
### AI & Search Tools
#### gpt-query
**Description:** Query GPT-5 directly with text input
**Command:** `~/tools/gpt-query "your question here"`
**Requirements:** OPENAI_API_KEY environment variable
**Examples:**
```bash
# Ask GPT-5 a question
~/tools/gpt-query "Explain quantum computing in simple terms"
# Get programming help
~/tools/gpt-query "How do I reverse a string in Python?"
```
#### google-search
**Description:** Search Google using Custom Search Engine with support for search operators
**Command:** `python ~/tools/google-search.py [OPTIONS] "query"`
**Requirements:** GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables
**Options:**
- `--json` - Output as JSON instead of formatted text
**Examples:**
```bash
# Basic search
python ~/tools/google-search.py "machine learning"
# Search specific site
python ~/tools/google-search.py "site:github.com python tensorflow"
# Search for PDFs
python ~/tools/google-search.py "filetype:pdf climate change"
# JSON output for programmatic use
python ~/tools/google-search.py "api documentation" --json
```
## Usage Pattern
At the start of each session, Claude Code reads this TOOLS.md file to understand available tools and their syntax. Tools are then executed directly via shell commands as needed throughout the work.
Example Tool Scripts
Here are the actual tool scripts referenced in the TOOLS.md above:
gpt-query tool (~/tools/gpt-query)
#!/bin/bash
# GPT Query Tool - Direct wrapper for Claude Code
cd ~/code/your-project-directory
if [ $# -eq 0 ]; then
echo "Usage: gpt-query \"your question here\""
exit 1
fi
query="$*"
/snap/bin/uv run --quiet python -c "
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv(override=True)
client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))
try:
resp = client.responses.create(
model='gpt-5',
input=[{'role': 'user', 'content': '''$query'''}],
)
print(getattr(resp, 'output_text', '') or '')
except Exception as e:
print(f'Error: {e}')
"
google-search tool (~/tools/google-search.py)
#!/usr/bin/env python3
"""
Google Custom Search Engine (CSE) Tool - Direct execution version
Provides search functionality using Google's Custom Search API with support for search dorks.
Returns minimal result data (title, url, description) to conserve context.
"""
import argparse
import json
import os
import requests
import sys
from typing import List, Optional
def search_google(query: str, num_results: int = 10) -> dict:
"""
Search Google using Custom Search Engine (CSE).
Supports Google search operators and dorks:
- site:example.com - Search specific site
- filetype:pdf - Search specific file types
- intitle:"exact phrase" - Search in titles
- inurl:keyword - Search in URLs
- "exact phrase" - Exact phrase matching
- keyword1 OR keyword2 - Boolean OR
- keyword1 -excluded - Exclude terms
Returns minimal result data to conserve context.
"""
api_key = os.getenv('GOOGLE_API_KEY')
cse_id = os.getenv('GOOGLE_CSE_ID') # Set your CSE ID as environment variable
if not api_key or not cse_id:
return {
'results': [],
'total_results': '0',
'search_time': '0',
'error': 'GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables required'
}
try:
url = "https://www.googleapis.com/customsearch/v1"
search_params = {
'key': api_key,
'cx': cse_id,
'q': query,
'num': min(num_results, 10) # API limit is 10
}
response = requests.get(url, params=search_params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
if 'items' in data:
for item in data['items']:
# Extract only essential fields to minimize output
result = {
'title': item.get('title', ''),
'url': item.get('link', ''),
'description': item.get('snippet', ''),
}
results.append(result)
search_info = data.get('searchInformation', {})
total_results = search_info.get('totalResults', '0')
search_time = search_info.get('searchTime', '0')
return {
'results': results,
'total_results': total_results,
'search_time': search_time
}
except requests.RequestException as e:
return {
'results': [],
'total_results': '0',
'search_time': '0',
'error': f'Request error: {str(e)}'
}
except Exception as e:
return {
'results': [],
'total_results': '0',
'search_time': '0',
'error': f'Unexpected error: {str(e)}'
}
def main():
parser = argparse.ArgumentParser(
description="Search Google using Custom Search Engine with support for search dorks",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Search operators supported:
site:example.com Search specific site
filetype:pdf Search specific file types
intitle:"exact phrase" Search in titles
inurl:keyword Search in URLs
"exact phrase" Exact phrase matching
keyword1 OR keyword2 Boolean OR
keyword1 -excluded Exclude terms
Examples:
%(prog)s "machine learning"
%(prog)s "site:github.com python tensorflow"
%(prog)s "filetype:pdf climate change" --num 5
%(prog)s 'intitle:"api documentation"' --json
"""
)
parser.add_argument(
'query',
type=str,
help='Search query with optional operators'
)
parser.add_argument(
'--num',
type=int,
default=10,
choices=range(1, 11),
metavar='1-10',
help='Number of results to return (default: 10)'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON instead of formatted text'
)
args = parser.parse_args()
try:
result = search_google(args.query, args.num)
if args.json:
# JSON output
print(json.dumps(result, indent=2))
else:
# Formatted text output
if 'error' in result:
print(f"Error: {result['error']}", file=sys.stderr)
sys.exit(1)
print(f"Search: {args.query}")
print(f"Results: {len(result['results'])} of {result['total_results']} ({result['search_time']}s)")
print("-" * 80)
for i, item in enumerate(result['results'], 1):
print(f"{i}. {item['title']}")
print(f" {item['url']}")
print(f" {item['description']}")
print()
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
How to set up a claude code startup hook
Note: This tutorial assumes you are running Claude Code in Linux / WSL / bash.
- Open Claude Code and run
/hooks:
Accessing the Hook Configuration screen by running /hooks. - On the Hook Configuration screen, select the SessionStart hook event:
Selecting the SessionStart hook event. - On the Tool Matchers screen, select 1. Add new matcher.
Adding a new matcher for startup configuration. - On the Add New Matcher screen, type
startupinto the “Tool matcher:” field.
Defining a tool matcher named “startup.” - On the SessionStart - Matcher screen, select 1. Add new hook.
Adding a new hook under the SessionStart matcher. - On the Event: SessionStart screen, type
cat ~/TOOLS.mdin the “Command:” field.
Setting the hook command to feed TOOLS.md into context. - On the Save Hook Configuration screen, confirm the details and choose where to save the hook. Choose 3. User settings to make the tools available generally.
Saving hook configuration to user settings. - Hit Esc a couple times until back at main Claude Code prompt. You should see
Added SessionStart hook: cat ~/TOOLS.md.
Confirmation that the startup hook has been added. - If you've placed your files and filled out your TOOLS.md, exit Claude Code and restart. BOOM! You should see the contents of your TOOLS.md in the output above your input prompt. You can now refer to the tools.
Verifying that TOOLS.md loads on startup and tools are available.